Manually setting Desktop vesion in React - reactjs

I have a custom Hook useWindowSize that determines whether there is a Mobile or Desktop environment. This is fine for most cases except when a mobile user wants to access the desktop version on his/her phone. I have a button to manually override the current windowsize but am not sure how to approach this.
Here I determine the opposite of the loaded windowsize but how can I switch and reload to the appropriate mode on click?
I will need this mode to stay afterwards even if the window is resized to keep the components linked to either mobile or desktop.
import "./styles.css";
import "./app.scss";
import useWindowSize from "./useWindowSize";
export default function App() {
const windowSize = useWindowSize();
const otherMode = windowSize <= "useMobileVersion" ? "useDesktopVersion" : "useDesktopVersion";
return (
<div className="App">
<p>Right now you are in {windowSize} mode. <button onClick={() => setPageMode("otherMode")}>
Switch to {otherMode} mode
</button>
</p>
</div>
);
}
The codesandbox is here.
The custom Hook is her:
import { useState, useEffect } from "react";
//a Util function that will convert the absolute width into breakpoints
function getBreakPoint(windowWidth) {
if (windowWidth) {
if (windowWidth < 420) {
return "useMobileVersion";
} else {
return "useDesktopVersion";
}
} else {
return "useDesktopVersion";
}
}
function useWindowSize() {
const isWindowClient = typeof window === "object";
const [windowSize, setWindowSize] = useState(
isWindowClient ? getBreakPoint(window.innerWidth) : undefined
);
useEffect(() => {
//a handler which will be called on change of the screen resize
function setSize() {
setWindowSize(getBreakPoint(window.innerWidth));
}
if (isWindowClient) {
//register the window resize listener
window.addEventListener("resize", setSize);
//unregister the listener
return () => window.removeEventListener("resize", setSize);
}
}, [isWindowClient, setWindowSize]);
return windowSize;
}
export default useWindowSize;

Related

Why lottieRef returns only null?

Using the library "# lottiefiles/react-lottie-player"
You need to get lottieRef to interact with animation, but I get null.
Code reference: https://codesandbox.io/s/great-rgb-7dp4j0?file=/src/HorizontalPicker/HorizontalPicker.jsx
import React, {useEffect, useRef} from "react";
import {Player} from "#lottiefiles/react-lottie-player";
export default function App() {
const player = useRef(null)
const lottie = useRef(null)
useEffect(() => {
if(lottie && lottie.current){
console.log(lottie.current) //return null
}
}, [])
return (
<div className="App">
<Player
lottieRef={data => lottie.current = data}
ref={player}
onEvent={event =>{
if(event === "load"){
lottie.current.play() //nothing
}
}}
keepLastFrame={true}
autoplay={false}
loop={true}
src={"https://lottie.host/2c01fd6c-437d-494e-af27-2a37e322bc60/prXv4Ic6px.json"}
style={{width: "100%", height: "2.5em", padding: "0", margin: "0"}}/>
</div>
);
}
There is also an interesting point.
If you output lottie, we get an object with null (while there is something inside it)
And if you output lottie.current, we get null.
Reference to an example of this thing: https://ibb.co/RQWxLkJ
Can you pass the lottie ref into your ref like the following (see ref on the div):
export default function App() {
const player = useRef(null)
const lottie = useRef(null)
useEffect(() => {
if(lottie && lottie.current){
console.log(lottie.current) //return null
}
}, [])
return (
<div className="App">
<Player
lottieRef={data => lottie.current = data}
ref={player}
onEvent={event =>{
if(event === "load"){
lottie.current && lottie.current.play() //nothing
}
}}
keepLastFrame={true}
autoplay={false}
loop={true}
src={"https://lottie.host/2c01fd6c-437d-494e-af27-2a37e322bc60/prXv4Ic6px.json"}
style={{width: "100%", height: "2.5em", padding: "0", margin: "0"}}/>
</div>
);
}
This way I am able to get the animation object when I console lottie.current
I'd prefer to use useState to save the animationData rather than a ref. But there was also an issue on the player where the 'load' event was firing but the player hadn't finish setting its internal instance of the animation therefor calling play() wouldn't work. This was happening only on functional components in React that's why it went undiscovered.
I've made a few changes to your code and to the player, using v1.5.2 you should be able to accomplish what you're looking for:
import css from "./HorizontalPicker.module.css";
import React, { useRef, useState, useEffect } from "react";
import { Player } from "#lottiefiles/react-lottie-player";
const HorizontalPicker = () => {
const player = useRef(null);
// const lottie = useRef(null);
const [lottie, setLottie] = useState(null);
useEffect(() => {
if (lottie) {
console.log(" Lottie animation data : ");
console.log(lottie);
// You can also play by calling the underlying lottie method
// lottie.play();
}
}, [lottie]);
return (
<>
<Player
onEvent={(event) => {
// console.log(event);
if (event === "instanceSaved" && player && player.current) {
console.log("Playing animation..");
player.current.play();
}
}}
lottieRef={(data) => {
setLottie(data);
}}
ref={player}
keepLastFrame={true}
autoplay={false}
loop={true}
src={
"https://lottie.host/2c01fd6c-437d-494e-af27-2a37e322bc60/prXv4Ic6px.json"
}
/>
</>
);
};
export default HorizontalPicker;
Sandbox:
https://codesandbox.io/s/lottie-react-functional-component-2bs8fs?file=/src/HorizontalPicker/HorizontalPicker.jsx
Cheers!
Well, according to official documentation https://github.com/LottieFiles/lottie-react, lottieRef represents a callback function which is fired by Player component (and this function returns AnimationFrame object)
I'm not familiar with this library, and whatever I'll describe next are just my assumptions :) Seems that whenever player "plays", it displays frames one-by-one (from json file in "src" attribute"). And whenever player displays frames from .json file - Player fires "lottieRef" event which you utilize to set lottie.current. And player starts displaying frames only when it loads .json data using "src" parameter in Player definition (see network tab in your browser to ensure that additional http request presents)
And in this case everything seems pretty logical: you try to access "lottie" variable in useEffect of your component but it's empty as far as Player did not manage to display any frame yet because the callback (lottieRef) did not fire yet as far as .json file is not loaded yet. No matter if .json file is large or small, Player requests it via additional http call, which requires some (even minital) amount time. And useEffect fires before .json is loaded immediately after rendering DOM (that's how ReactJS works)
On the other hand, if you delay a bit request to "lottie" ref - you'll see that it is populated:
Code example:
import React, { useEffect, useRef } from 'react';
import { Player } from '#lottiefiles/react-lottie-player';
export default function App() {
const player = useRef(null);
const lottie = useRef(null);
useEffect(() => {
setTimeout(() => console.log(lottie), 50);
}, []);
return (
<div className="App">
<Player
lottieRef={(data) => (lottie.current = data)}
ref={player}
keepLastFrame={true}
autoplay={false}
loop={true}
src={
'https://lottie.host/2c01fd6c-437d-494e-af27-2a37e322bc60/prXv4Ic6px.json'
}
/>
</div>
);
}
So if you delay onEvent callback event a bit, "lottie" ref would be initialized by that moment and .play() would work:
onEvent={(event) => {
event === 'load' &&
setTimeout(
() => lottie.current && lottie.current.play()
);
}
}
BUT, if the only purpose you have is to execute Player whenever it's ready - why not to use "autoplay" built-in property ? (autoplay={true})
import React, { useRef } from 'react';
import { Player } from '#lottiefiles/react-lottie-player';
export default function App() {
const player = useRef(null);
const lottie = useRef(null);
return (
<div className="App">
<Player
lottieRef={(data) => (lottie.current = data)}
ref={player}
keepLastFrame={true}
autoplay={true}
loop={true}
src={
'https://lottie.host/2c01fd6c-437d-494e-af27-2a37e322bc60/prXv4Ic6px.json'
}
/>
</div>
);
}
Hope it'll help

how to register onClick and onDoubleClick event from same JSX element in React Functional Component?

Is there a proper , established way to register either User permormed single click or double click on same JSX element inside of Function component. After reading articles on stackOverflow and watching youtube, the easiest solution for beginner like me was to create custom hook - useClickHook, and to use callback inside setTimeout api. In App component I'm using useEffect hook,
clickHook value is inside of array of dependencies. On first render its 0 , after first click = 1 , if doubleClick = 2; inside of If() statement in useeffect - console.log() reperesents function to be invoked. and after i'm setting clickHook value back to default 0.
Here is what I coded (minimal reproducible example)
import { useState, useEffect } from 'react';
export const useClickHook = (detail) => {
const [clickDetail, setClickDetail] = useState(0);
useEffect(() => {
const timer = setTimeout(() => {
setClickDetail(detail)
}, 200);
return () => {
clearTimeout(timer);
}
}, [detail]);
return clickDetail;
}
import { useEffect, useState } from 'react';
import { useClickHook } from './ClickHook';
function App() {
const [click, setClick] = useState(0);
const clickHook = useClickHook(click);
useEffect(() => {
if (clickHook === 1) {
console.log('single click')
}
if (clickHook === 2) {
console.log('double click')
}
setClick(0);
},[clickHook])
return (
<div className="App">
Hello World
<button
onClick={(e) => {
setClick(e.detail);
}}
onDoubleClick={(e) => {
setClick(e.detail);
}}
>
Click
</button>
</div>
);
}
export default App;
How to improve it?
Will be glad for every suggestions.
Edited!!!
So one way to improve is simple to remove onDoubleClick event handler from button JSX element .
Following code can be safely deleted
onDoubleClick={(e) => {
setClick(e.detail);
}}
Thus , on double click setClick will be called only twice , not 3 times like in example proposed originaly , and the rest will be done by the custom hook as before.
Pretty sure you can already do this in React with no special code required.
Demo here
export default function Demo() {
const handleClick = (event) => {
console.log(event.detail);
switch (event.detail) {
case 1: {
console.log("single click");
break;
}
case 2: {
console.log("double click");
break;
}
default: {
break;
}
}
};
return (
<div>
<div>
<button onClick={handleClick}>Double click</button>
</div>
</div>
);
}

Two React useEffect hooks accessing the window onscroll?

I am new to React dev so this may be something simple I am missing with hooks.
Using a template, I have used a header bar which shrinks in height if you scroll down in the page far enough (i.e it is only at max height if you scroll to the top).
I have been customising a sidebar to go along with the headerbar, and I'm trying to get the items within it to also move up when the bottom of the headerbar moves up.
The app bar uses a pre-made function:
import { useState, useEffect } from 'react';
// ----------------------------------------------------------------------
export default function useOffSetTop(top: number) {
const [offsetTop, setOffSetTop] = useState(false);
const isTop = top || 100;
useEffect(() => {
window.onscroll = () => {
if (window.pageYOffset > isTop) {
setOffSetTop(true);
} else {
setOffSetTop(false);
}
};
return () => {
window.onscroll = null;
};
}, [isTop]);
return offsetTop;
}
Then you can just import it, assign a constant bool to useOffSetTop(HEADER.DASHBOARD_DESKTOP_HEIGHT) and base the layout on the state of that const.
In the app bar it controls the height, so in the nav bar I made it control he height of an empty .
It does work, but the app bar stops working.
I do have hot-reload on and if I make a change to the app bar it starts working but the nav bar stops working.
I guess it is just because whichever loads last is the one which binds something to window.onscroll and the other is wiped.
I am just wondering how I could change this function or restructure the code so that this could be imported by multiple components on the same page - possibly without having to just import it higher up and pass the true/false value down through the components?
The issue is that you are actually "overriding" the onScroll function (or replacing it) instead of listening for the event.
by doing this
window.onScroll = null;
you are effectively overriding the onScroll function to do nothing.
Best to listen for the onscroll event.
import { useState, useEffect } from 'react';
export default function useOffSetTop(top: number) {
const [offsetTop, setOffSetTop] = useState(false);
const isTop = top || 100;
const handleOnScroll = () => {
if (window.pageYOffset > isTop) {
setOffSetTop(true);
} else {
setOffSetTop(false);
}
}
useEffect(() => {
window.addEventListener('scroll', handleOnScroll )
return () => {
window.removeEventListener('scroll', handleOnScroll)
};
}, [isTop, handleOnScroll]);
return offsetTop;
}

How to set window resize event listener value to React State?

This issue is very simple but I probably overlook very little point. Window screen size is listening by PostLayout component. When window width is less than 768px, I expect that isDesktopSize is false. I tried everything like using arrow function in setIsDesktopSize, using text inside of true or false for state value, using callback method etc... but it's not working.
PostLayout shared below:
import React, {useState,useEffect, useCallback} from 'react'
import LeftSideNavbar from './LeftSideNavbar'
import TopNavbar from './TopNavbar'
export default function PostLayout({children}) {
const [isDesktopSize, setIsDesktopSize] = useState(true)
let autoResize = () => {
console.log("Desktop: " + isDesktopSize);
console.log(window.innerWidth);
if(window.innerWidth < 768 ){
setIsDesktopSize(false)
}else{
setIsDesktopSize(true)
}
}
useEffect(() => {
window.addEventListener('resize', autoResize)
autoResize();
}, [])
return (
<>
<TopNavbar isDesktopSize={isDesktopSize}/>
<main>
<LeftSideNavbar/>
{children}
</main>
</>
)
}
console log is shared below:
Desktop: true
627
This could probably be extracted into a custom hook. There's a few things you'd want to address:
Right now you default the state to true, but when the component loads, that may not be correct. This is probably why you see an incorrect console log on the first execution of the effect. Calculating the initial state to be accurate could save you some jank/double rendering.
You aren't disconnecting the resize listener when the component unmounts, which could result in an error attempting to set state on the component after it has unmounted.
Here's an example of a custom hook that addresses those:
function testIsDesktop() {
if (typeof window === 'undefined') {
return true;
}
return window.innerWidth >= 768;
}
function useIsDesktopSize() {
// Initialize the desktop size to an accurate value on initial state set
const [isDesktopSize, setIsDesktopSize] = useState(testIsDesktop);
useEffect(() => {
if (typeof window === 'undefined') {
return;
}
function autoResize() {
setIsDesktopSize(testIsDesktop());
}
window.addEventListener('resize', autoResize);
// This is likely unnecessary, as the initial state should capture
// the size, however if a resize occurs between initial state set by
// React and before the event listener is attached, this
// will just make sure it captures that.
autoResize();
// Return a function to disconnect the event listener
return () => window.removeEventListener('resize', autoResize);
}, [])
return isDesktopSize;
}
Then to use this, your other component would look like this (assuming your custom hook is just in this same file -- though it may be useful to extract it to a separate file and import it):
import React, { useState } from 'react'
import LeftSideNavbar from './LeftSideNavbar'
import TopNavbar from './TopNavbar'
export default function PostLayout({children}) {
const isDesktopSize = useIsDesktopSize();
return (
<>
<TopNavbar isDesktopSize={isDesktopSize}/>
<main>
<LeftSideNavbar/>
{children}
</main>
</>
)
}
EDIT: I modified this slightly so it should theoretically work with a server-side renderer, which will assume a desktop size.
Try this, you are setting isDesktopSizze to 'mobile', which is === true
const [isDesktopSize, setIsDesktopSize] = useState(true)
let autoResize = () => {
console.log("Desktop: " + isDesktopSize);
console.log(window.innerWidth);
if(window.innerWidth < 768 ){
setIsDesktopSize(true)
}else{
setIsDesktopSize(false)
}
}
I didn't find such a package on npm and I thought it would be nice to create one: https://www.npmjs.com/package/use-device-detect. I think it will help someone :)

How can I prevent the MaterialUI Switch from animating?

I'm new to React and making a Chrome extension with it.
Currently, I am using the Switch component from MaterialUI inside my popup page. How I am saving its state right now is by storing the state of each change in chrome.storage.local API. When I click back to the pop-up, I simply use the useEffect hook & fetch the state from chrome.storage.local & pass it as an argument to setState().
My issue with this is that it causes the toggle button to animate from off to on very briefly when you reopen the popup (as if you were manually toggling it). I'm aware it's because of the way I'm doing it (i.e, initializing the state of the toggle as false each time the pop-up is opened) but I'm currently stumped on doing this another way. Could anyone please help me? Thanks for reading!
MySwitchComponent.jsx
import React, { useState } from "react";
import { withStyles } from "#material-ui/core/styles";
import { Switch } from '#material-ui/core/';
const StyledSwitch = withStyles({
root: {
position: 'relative',
marginTop: '20px',
marginLeft: '90px'
},
})(Switch);
export default function NewSwitch() {
const [state, setState] = React.useState(false)
const handleChange = (event) => {
setState(event.target.checked);
chrome.storage.local.set({auto_delete_toggle: event.target.checked });
}
React.useEffect(() => {
chrome.storage.local.get(null, function(res){
setState(res.auto_delete_toggle);
})
});
return (
<StyledSwitch
checked={state}
onChange={handleChange}
>
</StyledSwitch>)
}
My popup.js just renders all the components encapsulated in a single in popup.html. Also, chrome.storage.local.get is asynchronous.
EDIT:
Here is a GIF to better illustrate my issue:
First, here's a sandbox that reproduces your issue. I've used a mockAsyncStorage (which is backed by window.localStorage) to mimic the async chrome.storage.local.get. You can see the undesired transition by clicking the switch so that it is checked, and then refreshing the page.
import React from "react";
import Switch from "#material-ui/core/Switch";
import mockAsyncStorage from "./mockAsyncStorage";
export default function App() {
const [checked, setChecked] = React.useState(false);
const handleChange = event => {
setChecked(event.target.checked);
mockAsyncStorage.set({ auto_delete_toggle: event.target.checked });
};
React.useEffect(() => {
mockAsyncStorage.get(null, function(res) {
setChecked(
res.auto_delete_toggle === undefined ? false : res.auto_delete_toggle
);
});
}, []);
return <Switch checked={checked} onChange={handleChange} />;
}
Below is one way to get rid of the transition. This initializes checked to undefined and while it is still undefined, it returns null instead of the switch so nothing is rendered until the appropriate initial state of the switch is known.
import React from "react";
import Switch from "#material-ui/core/Switch";
import mockAsyncStorage from "./mockAsyncStorage";
export default function App() {
const [checked, setChecked] = React.useState(undefined);
const handleChange = event => {
setChecked(event.target.checked);
mockAsyncStorage.set({ auto_delete_toggle: event.target.checked });
};
React.useEffect(() => {
mockAsyncStorage.get(null, function(res) {
setChecked(
res.auto_delete_toggle === undefined ? false : res.auto_delete_toggle
);
});
}, []);
if (checked === undefined) {
return null;
}
return <Switch checked={checked} onChange={handleChange} />;
}

Resources