Cannot use useState within a function because of asynchronous - reactjs

I am trying to make a like/unlike button and for this purpose, I use a react hook name liked initialised to false.
this hook is used to modify the like button in front and the like event in back-end.
The problem is that setState is an asynchron function and I cannot have the good state of liked to perform my actions.
I already tried with a useEffect but with liked initialised to false, the action when liked === false is performed on loading. I don t want to.
here is my code
import React from 'react'
import styled from 'styled-components'
import HeartIcon from 'client/components/icons/Heart'
import IconButton from 'client/components/IconButton'
const Heart = styled(HeartIcon)`
stroke: ${p => p.theme.primary};
stroke-width: 2;
fill: transparent;
transition: fill 300ms;
display: block;
width: 100%;
height: auto;
&[aria-checked='true'] {
fill: ${p => p.theme.primary};
}
`
export default function LikeButton(props) {
const [liked, setLiked] = React.useState(false)
function onLikeChange() {
setLiked(prevLiked => !prevLiked)
if (liked === true) {
// creation d'un event like
console.log('like')
} else {
console.log('unlike')
// destroy l'event du like existant
}
}
return (
<IconButton onClick={onLikeChange} {...props}>
<Heart aria-checked={liked} />
</IconButton>
)
}
of course I can switch my actions to perform what I want but I prefere to understand what I'm doing because I'm new to react ;)
What is the way ? Thank you

I think what you are trying to accomplish here needs two useEffect hooks. One for getting the value from the backend on initial load and one for updating the value when it changes. For this you should use two useEffect hooks. The difference between these is quite simple. The hook for setting the initial value is for when your component needs to do something after render, and the hook for setting the liked value when it the state changes is only called when liked changes. Therefor you pass an array as an optional second argument to useEffect.
const [liked, setLiked] = useState()
useEffect(() => {
console.log('get the initial value of liked from backend');
setLiked(initialValue)
}
useEffect(() => {
console.log('Do something after liked has changed', liked);
if (liked === true) {
console.log('like')
} else {
console.log('unlike')
}
}, [liked]);
function onLikeChange() {
setLiked(prevLiked => !prevLiked)
}

Related

How to change the background of a styled.div if the window is scrolled to the top?

I would like to change the background of a div if the window is scrolled to the top. I have figured out how to determine that in React, and when console.logging my isAtTop variable it will change based on the scroll, however the actual div never seems to receive as the styled.div doesn't change colors. I have made a codePen: https://codesandbox.io/s/xenodochial-chebyshev-4poi3?file=/src/App.js
import "./styles.css";
import React from 'react'
import styled from 'styled-components'
export default function App() {
let isAtTop = true;
let bg = `linear-gradient(to right, #797cd2, #393e9e)`;
function handleScroll() {
isAtTop = window.scrollY === 0;
isAtTop
? (bg = `linear-gradient(to right, #797cd2, #393e9e)`)
: (bg = "white");
console.log({ isAtTop });
}
React.useEffect(() => {
console.log("UseEffect Run");
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
});
const ColorChangeDiv = styled.div`
background: ${bg};
position: sticky;
width: 100%;
top: 0;
height: 100px;
`
return (
<>
<ColorChangeDiv>
{isAtTop.toString()} ? Hello my background should be {bg}
</ColorChangeDiv>
<div style={{height: '500rem'}}></div>
</>
);
}
I changed your code a little, now I will explain what exactly changed.
Made the functions arrow, so it looks cleaner
Wrapped the scroll event handler in useCallback to exclude unnecessary calls to useEffect, since now this function is in useEffect dependencies and it is a good practice to specify all dependencies used in useEffect.
The background color is now stored in the state. You were storing in a variable, the component is not re-rendered on changing the let variable. This means that even changing the value of the variable, the component will not display itself with the new values. We need a rerender. A change in state causes a rerender. This means that if you want to change the component depending on the changed values, then store them in a state to call the rerender :)
I brought the styled component out, it looks cleaner and plus, it makes no sense to keep it inside the APP component, you can pass the values ​​to the styled component in props. Check out the example below, now the background is passed as props
You have long default background values. And it repeats itself. Such long repetitive things are better to be translated into constants with good understandable names. See I brought it up as DEFAULT_BG.
https://codesandbox.io/s/boring-pasteur-18v0r?file=/src/App.js:361-372
import React, { useCallback, useState, useEffect } from "react";
import styled from "styled-components";
const DEFAULT_BG = `linear-gradient(to right, #797cd2, #393e9e)`;
const ColorChangeDiv = styled.div`
background: ${(p) => p.bg};
position: sticky;
width: 100%;
top: 0;
height: 2000px;
`;
const App = () => {
const [bg, setBg] = useState(DEFAULT_BG);
const handleScroll = useCallback(() => {
setBg(window.scrollY === 0 ? DEFAULT_BG : "white");
}, []);
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, [handleScroll]);
return (
<>
<ColorChangeDiv bg={bg}>
Hello my background should be {bg}
</ColorChangeDiv>
<div style={{ height: "500rem" }}></div>
</>
);
};
export default App;

nextjs react recoil persist values in local storage: initial page load in wrong state

I have the following code,
const Layout: React.FC<LayoutProps> = ({ children }) => {
const darkMode = useRecoilValue(darkModeAtom)
console.log('darkMode: ', darkMode)
return (
<div className={`max-w-6xl mx-auto my-2 ${darkMode ? 'dark' : ''}`}>
<Nav />
{children}
<style jsx global>{`
body {
background-color: ${darkMode ? '#12232e' : '#eefbfb'};
}
`}</style>
</div>
)
}
I am using recoil with recoil-persist.
So, when the darkMode value is true, the className should include a dark class, right? but it doesn't. I don't know what's wrong here. But it just doesn't work when I refresh for the first time, after that it works fine. I also tried with darkMode === true condition and it still doesn't work. You see the styled jsx, that works fine. That changes with the darkMode value and when I refresh it persists the data. But when I inspect I don't see the dark class in the first div. Also, when I console.log the darkMode value, I see true, but the dark class is not included.
Here's the sandbox link
Maybe it's a silly mistake, But I wasted a lot of time on this. So what am I doing wrong here?
The problem is that during SSR (server side rendering) there is no localStorage/Storage object available. So the resulted html coming from the server always has darkMode set to false. That's why you can see in cosole mismatched markup errors on hydration step.
I'd assume using some state that will always be false on the initial render (during hydration step) to match SSR'ed html but later will use actual darkMode value. Something like:
// themeStates.ts
import * as React from "react";
import { atom, useRecoilState } from "recoil";
import { recoilPersist } from "recoil-persist";
const { persistAtom } = recoilPersist();
export const darkModeAtom = atom<boolean>({
key: "darkMode",
default: false,
effects_UNSTABLE: [persistAtom]
});
export function useDarkMode() {
const [isInitial, setIsInitial] = React.useState(true);
const [darkModeStored, setDarkModeStored] = useRecoilState(darkModeAtom);
React.useEffect(() => {
setIsInitial(false);
}, []);
return [
isInitial === true ? false : darkModeStored,
setDarkModeStored
] as const;
}
And inside components use it like that:
// Layout.tsx
const [darkMode] = useDarkMode();
// Nav.tsx
const [darkMode, setDarkMode] = useDarkMode();
codesandbox link
Extending on #aleksxor solution, you can perform the useEffect once as follows.
First create an atom to handle the SSR completed state and a convenience function to set it.
import { atom, useSetRecoilState } from "recoil"
const ssrCompletedState = atom({
key: "SsrCompleted",
default: false,
})
export const useSsrComplectedState = () => {
const setSsrCompleted = useSetRecoilState(ssrCompletedState)
return () => setSsrCompleted(true)
}
Then in your code add the hook. Make sure it's an inner component to the Recoil provider.
const setSsrCompleted = useSsrComplectedState()
useEffect(setSsrCompleted, [setSsrCompleted])
Now create an atom effect to replace the recoil-persist persistAtom.
import { AtomEffect } from "recoil"
import { recoilPersist } from "recoil-persist"
const { persistAtom } = recoilPersist()
export const persistAtomEffect = <T>(param: Parameters<AtomEffect<T>>[0]) => {
param.getPromise(ssrCompletedState).then(() => persistAtom(param))
}
Now use this new function in your atom.
export const darkModeAtom = atom({
key: "darkMode",
default: false,
effects_UNSTABLE: [persistAtomEffect]
})

Ensure entrance css transition with React component on render

I am trying to build a barebones css transition wrapper in React, where a boolean property controls an HTML class that toggles css properties that are set to transition. For the use case in question, we also want the component to be unmounted (return null) before the entrance transition and after the exit transition.
To do this, I use two boolean state variables: one that controls the mounting and one that control the HTML class. When props.in goes from false to true, I set mounted to true. Now the trick: if the class is set immediate to "in" when it's first rendered, the transition does not occur. We need the component to be rendered with class "out" first and then change the class to "in".
A setTimeout works but is pretty arbitrary and not strictly tied to the React lifecycle. I've found that even a timeout of 10ms can sometimes fail to produce the effect. It's a crapshoot.
I had thought that using useEffect with mounted as the dependency would work because the component would be rendered and the effect would occur after:
useEffect(if (mounted) { () => setClass("in"); }, [mounted]);
(see full code in context below)
but this fails to produce the transition. I believe this is because React batches operations and chooses when to render to the real DOM, and most of the time doesn't do so until after the effect has also occurred.
How can I guarantee that my class value is change only after, but immediately after, the component is rendered after mounted gets set to true?
Simplified React component:
function Transition(props) {
const [inStyle, setInStyle] = useState(props.in);
const [mounted, setMounted] = useState(props.in);
function transitionAfterMount() {
// // This can work if React happens to render after mounted get set but before
// // the effect; but this is inconsistent. How to wait until after render?
setInStyle(true);
// // this works, but is arbitrary, pits UI delay against robustness, and is not
// // tied to the React lifecycle
// setTimeout(() => setInStyle(true), 35);
}
function unmountAfterTransition() {
setTimeout(() => setMounted(false), props.duration);
}
// mount on props.in, or start exit transition on !props.in
useEffect(() => {
props.in ? setMounted(true) : setInStyle(false);
}, [props.in]);
// initiate transition after mount
useEffect(() => {
if (mounted) { transitionAfterMount(); }
}, [mounted]);
// unmount after transition
useEffect(() => {
if (!props.in) { unmountAfterTransition(); }
}, [props.in]);
if (!mounted) { return false; }
return (
<div className={"transition " + inStyle ? "in" : "out"}>
{props.children}
</div>
)
}
Example styles:
.in: {
opacity: 1;
}
.out: {
opacity: 0;
}
.transition {
transition-property: opacity;
transition-duration: 1s;
}
And usage
function Main() {
const [show, setShow] = useState(false);
return (
<>
<div onClick={() => setShow(!show)}>Toggle</div>
<Transition in={show} duration={1000}>
Hello, world.
</Transition>
<div>This helps us see when the above component is unmounted</div>
</>
);
}
Found the solution looking outside of React. Using window.requestAnimationFrame allows an action to be take after the next DOM paint.
function transitionAfterMount() {
// hack: setTimeout(() => setInStyle(true), 35);
// not hack:
window.requestAnimationFrame(() => setInStyle(true));
}

gatsby + react hook masonry breaks on build because there is no window

I have a react hook set up to handle masonry on certain pages of my gatsby site. The problem is it references the window object, which does not exist on the server side gatsby build. I've read that the solution is to wrap useEffect with:
if (typeof window === 'undefined') {
}
however I just can't seem to wrap the right part of my masonry file. I've also read that using the above hack makes the server side rendering sort of pointless, not sure.
Could someone tell me where that if statement should go in my masonry file below? It's not a plugin, it's a hook in my utils folder. Using code from this tut. I've tried the if statement inside the useEffects, around the useEffects, around the whole eventListener, but no dice. Thank you!!
import React, { useEffect, useRef, useState } from "react"
import styled from "styled-components"
const useEventListener = (eventName, handler, element = window) => {
const savedHandler = useRef()
useEffect(() => {
savedHandler.current = handler
}, [handler])
useEffect(() => {
const isSupported = element && element.addEventListener
if (!isSupported) return
const eventListener = event => savedHandler.current(event)
element.addEventListener(eventName, eventListener)
return () => {
element.removeEventListener(eventName, eventListener)
}
}, [eventName, element])
}
const fillCols = (children, cols) => {
children.forEach((child, i) => cols[i % cols.length].push(child))
}
export default function Masonry({ children, gap, minWidth = 500, ...rest }) {
const ref = useRef()
const [numCols, setNumCols] = useState(3)
const cols = [...Array(numCols)].map(() => [])
fillCols(children, cols)
const resizeHandler = () =>
setNumCols(Math.ceil(ref.current.offsetWidth / minWidth))
useEffect(resizeHandler, [])
useEventListener(`resize`, resizeHandler)
const MasonryDiv = styled.div`
margin: 1rem auto;
display: grid;
grid-auto-flow: column;
grid-gap: 1rem;
`
const Col = styled.div`
display: grid;
grid-gap: 1rem;
`
return (
<MasonryDiv ref={ref} gap={gap} {...rest}>
{[...Array(numCols)].map((_, index) => (
<Col key={index} gap={gap}>
{cols[index]}
</Col>
))}
</MasonryDiv>
)
}
In your gatsby-node.js add the following snippet:
exports.onCreateWebpackConfig = ({ stage, loaders, actions }) => {
if (stage === "build-html") {
actions.setWebpackConfig({
module: {
rules: [
{
test: /masonry/,
use: loaders.null(),
},
],
},
})
}
}
Note: use /masonry/ library path from your node_modules.
From Gatsby documentation about debugging HTML builds:
Errors while building static HTML files generally happen for one of
the following reasons:
Some of your code references “browser globals” like window or
document. If this is your problem you should see an error above like
“window is not defined”. To fix this, find the offending code and
either a) check before calling the code if window is defined so the
code doesn’t run while Gatsby is building (see code sample below) or
b) if the code is in the render function of a React.js component, move
that code into a componentDidMount lifecycle or into a useEffect hook,
which ensures the code doesn’t run unless it’s in the browser.
Alternatively, you can wrap your import your Masonry hook usage inside this statement:
if (typeof window !== `undefined`) {
const module = require("module")
}
Note the !== comparison, not the === like the one you've provided.

How state of a react hook must update if it has Map?

I have multiple(more than 15) div tags as tiles. I need to emphasis each one if mouse hover on it. So each tag has onMouseEnter/Leave functions as bellow.
<div
key={key}
onMouseEnter={onMouseEnter(key)}
onMouseLeave={onMouseLeave(key)}
>
...
</div>
Also I put each tiles key in a Map data structure.
const onMouseEnter = key => {
return function() {
const newIsHover = new Map(isHover)
newIsHover.set(key, true)
setIsHover(newIsHover)
}
}
const onMouseLeave = key => {
return function() {
const newIsHover = new Map(isHover)
newIsHover.delete(key)
setIsHover(newIsHover)
}
}
Since component is hook it put its state in a useState.
const [isHover, setIsHover] = useState(new Map())
What is happening here:
Always I enter a tile: onMouseEnter function called and its key added to map (as expected)
When I leave a tile: always onMouseLeave called but sometimes key is removed (as expected) and tile turned back to its normal shape but sometimes it does not(problem is here, in this situation map updated at setIsHover in onMouseLeave but it does not changed in the component!).
I think map updated as expected but when I move on new tile it does not understand that yet. So it overwrite it with what it has.
PS: example added. Move between tiles with high speed!
Like the class-based components, calls to update state are asynchronous and get queued up. Try using functional state updates to ensure these queued-up updates correctly update the previous state. This should fix race conditions between quick successive setIsHover calls with the same key.
Notice if you move slowly enough between tiles they correctly highlight and unhighlight, but more quickly (like a swipe) and 2 or more can get stuck until you again slowly exit the tile.
const onMouseEnter = key => {
return function() {
setIsHover(prevIsHover => {
const newIsHover = new Map(prevIsHover);
newIsHover.set(key, true);
return newIsHover;
});
}
}
const onMouseLeave = key => {
return function() {
setIsHover(prevIsHover => {
const newIsHover = new Map(prevIsHover);
newIsHover.delete(key);
return newIsHover;
});
}
}
But I should note that this is a lot of leg work for simply applying some component styling, especially hovering. It could more simply be achieved using CSS.
tileStyles.css
.tile {
background-color: lightgray;
border: 3px solid black;
height: 100px;
line-height: 100px;
margin: 10px;
text-align: center;
width: 100px;
}
.tile:hover {
border-color: red;
}
tile.jsx
import React from "react";
import { withStyles } from "#material-ui/core";
import "./tileStyles.css";
const styles = {
container: { display: "flex", width: "600px", flexWrap: "wrap" }
};
const Tiles = ({ classes: { container }, tiles }) => {
return (
<div className={container}>
{tiles.map((tl, key) => {
return (
<div className="tile" key={key} name={key}>
hi
</div>
);
})}
</div>
);
};
export default withStyles(styles)(Tiles);
The normal and hovered styles are applied together (at the same time) and CSS/html will manage when it hovered or not. The component no longer requires event listeners and doesn't need to maintain internal state.
Explanation
what means "...calls to update state are asynchronous and get queued up."?
When you call this.setState or a useState update function the update doesn't happen synchronously right then and there, but they are queued up during the current render cycle and batch processed in the order in which they were queued. Perhaps this demo will help illustrate what happens. What confounds this issue is the fact that event processing is also asynchronous, meaning that, when events occur their registered callbacks are placed in the event queue to be processed.

Resources