React fader component fade direction not changing - reactjs

I'm trying to create a small component that will fade between it's children when one of its methods is called. I've been following this code, but so it supports any number of children. So far, I have this:
export const Fader = React.forwardRef<FaderProps, Props>((props, ref) => {
const children = React.Children.toArray(props.children);
const [currentChild, setCurrentChild] = useState({
child: props.startIndex || 0,
direction: 1,
});
let nextChild = 0;
const fadeNext = (): void => {
queueNextFade(); //Queues the next fade which fades in the next child after the current child has faded out
setCurrentChild({
child: currentChild.child,
direction: +!currentChild.direction,
});
nextChild = currentChild.child + 1;
}
const fadePrev = (): void => {
}
const fadeTo = (index: number): void => {
}
const queueNextFade = (): void => {
setTimeout(() => {
setCurrentChild({
child: nextChild,
direction: +!currentChild.direction,
});
}, props.fadeTime || 500)
}
useImperativeHandle(ref, () => ({ fadeNext, fadePrev, fadeTo }));
return (
<div>
{
React.Children.map(children, (child, i) => (
<div key={i}
style={{
opacity: i === currentChild.child ? currentChild.direction : "0",
transition: `opacity ${props.fadeTime || 500}ms ease-in`,
}}
>
{child}
</div>
))
}
</div>
)
});
Logic wise it does work, but what actually happens is that the first child fades out, but the next child doesn't fade in. If faded again, the second child fades in then fades out, and the next child fades in.
(View in sandbox)
For a while I was confused as to why that was happening because I'm using the same logic as the other library. I did some research on seeing if I could make useState be instant and I came across this post and I quote from it:
Even if you add a setTimeout the function, though the timeout will run after some time by which the re-render would have happened, the setTimeout will still use the value from its previous closure and not the updated one.
which I realised is what's happening in my situation. I start the setTimeout in which currentChild.direction is 1. Then a state change happens and the direction changes to 0. A long time later the setTimeout finishes but instead of changing the direction from 0 to 1, it changes from 1 to 0 because it kept it original value from when it was first called, hence why the second child doesn't fade in and just stays invisible.
I could just change it to:
let currentChild = {...}
and the have a "blank" useState to act as a forceUpdate but I know force updates are against React's nature and there's probably a better way to do this anyway.
If anyone could help out, I would appreaciate it

For anyone in the future, I found this post explaning that setTimeout will use the value from the initial render of the component
To fix this, I changed my setTimeout to:
setTimeout(() => {
setCurrentChild(currentChild => ({
child: nextChild,
direction: +!currentChild.direction,
}));
}, props.fadeTime || 500)

Related

React - child not reverting to parent state when child state not saved

I fear that I've made a complete mess of my React structure.
I have a parent component/view that contains an object of attributes that are the key element of the content on the page. The ideal is to have a component that allows the user to update these attributes, and when they do so the content on the page updates. To avoid having the content update on every single attribute update/click, I want to implement a 'revert'/'save' button on the attribute update screen.
The problem I'm having is that I'm passing the state update function from the main parent to the child components, and the save is updating the parent and then the child switches out of 'edit mode' and displays the correct value, but when I click revert it doesn't update the parent (good) but it still maintains the local state of the child rather than rerendering to the 'true' parent state so the child implies it has been updated when it actually hasn't.
I'm a hobbyist React developer so I'm hoping that there is just something wrong with my setup that is easily rectifiable.
export const MainViewParent = () => {
const [playerAttributes, updatePlayerAttributes] = useState(basePlayerAttributes);
const revertPlayerAttributes = () => {
updatePlayerAttributes({...playerAttributes});
};
// Fill on first mount
useEffect(() => {
// Perform logic that uses attributes here to initialise page
}, []);
const adjustPlayerAttributes = (player, newAttributes) => {
// Update the particular attributes
playerAttributes[player] = newAttributes;
updatePlayerAttributes({...playerAttributes});
};
return (
<PlayerAttributes
playerAttributes={playerAttributes}
playerNames={["Player 1"]}
playerKeys={["player1"]}
updatePlayerAttributes={adjustPlayerAttributes} // Update the 'base' attributes, requires player key - uses function that then calls useState update function
revertPlayerAttributes={revertPlayerAttributes}
/>
);
}
// Child component one - renders another child component for each player
export const PlayerAttributes = ({
updatePlayerAttributes
}) => {
return (
<AnotherChildComponent />
);
};
// Child component 2
const AnotherChildComponent = ({ ...someThings, updatePlayerAttributes }) => {
const [editMode, updateEditMode] = useState(false); // Toggle for showing update graph elements
const [local, updateLocal] = useState({...playerAttributes}); // Store version of parent data to store changes prior to revert/save logic
// Just update the local state as the 'save' button hasn't been pressed
const updateAttributes = ( attributeGroup, attributeKey, attributeValue) => {
local[attributeGroup][attributeKey] = attributeValue;
updateLocal({...local});
};
const revertAttributes = () => {
// Need to update local back to the original state of playerAttributes
updateLocal(playerAttributes);
// revertPlayerAttributes();
updateEditMode(false); // Toggle edit mode back
};
const saveDetails = () => {
updatePlayerAttributes(playerKey, local);
updateEditMode(false);
};
return (
<FormElement
editMode={editMode}
// Should pass in a current value
playerAttributes={playerAttributes}
attributeOptions={[0.2, 0.3, 0.4, 0.5, 0.6]}
updateFunction={updateAttributes} // When you click update the local variable
/> // This handles conditional display of data depending on editMode above
);
}
// Child component 3...
const FormElement = ({ editMode, playerAttributes, attributeOptions, updateFunction, attributeGroup, attributeKey }) => {
if (editMode) {
return (
<div>
{attributeOptions.map(option =>
<div
key={option}
onClick={() => updateFunction(attributeGroup, attributeKey, option)}
>
{option)
</div>
)}
</div>
);
}
return (
<div>{//attribute of interest}</div>
);
};
I'm just a bit confused about as to why the revert button doesn't work here. My child displays and updates the information that is held in the parent but should be fully controlled through the passing of the function and variable defined in the useState call at the top of the component tree shouldn't it?
I've tried to cut down my awful code into something that can hopefully be debugged! I can't help but feel it is unnecessary complexity that is causing some issues here, or lifecycle things that I don't fully grasp. Any advice or solutions would be greatly appreciated!

how to get the height of the children from a parent? - React

Sorry for the question.
I am new to react.
I want to get the height of each child then apply the maximum one on all of them.
by the way i do it, i get always the height of the last child.
on other hand i don't know exactly how to force the maximum height on all the children.
i really appreciate the help.
here is my code :
for the parent component :
export default function Parent({data, index}) {
const [testHeight, setTestHeight] = useState([]);
const listRef = useRef(null);
useEffect (() => {
setTestHeight(listRef.current.clientHeight)
})
const {
objects: blocs
} = data
return (
<>
{blocs && Object.values(blocs).map((itemsBlocks, i) => (
<ItemChild dataItem={itemsBlocks}
ref={listRef}
maxHeight= { testHeight}
/>
))}
</>
)
}
for the child component :
const Child = forwardRef(function Child ({dataItem, maxHeight}, ref) {
useEffect(() => {
console.log(ref.current.clientHeight);
})
const {
description,
title
} = dataItem || {}
return (
<>
<div className="class_child" ref={ref} >
{maxHeight}
<p> {title} </p>
<p> {description} </p>
</div>
</>
)
});
export default Child
You have multiple issues.
First off, you are storing the height, from the parent, and, you seem to be giving the same ref to all of your children ?
Each component should have their own ref.
If you want to get all height, you need to change this :
useEffect (() => {
setTestHeight(listRef.current.clientHeight)
})
You are replacing the value of your testHeight unstead of adding values to the array.
You should instead, do this :
useEffect (() => {
setTestHeight((current ) => [...current, listRef.current.clientHeight])
})
I advise you to look how to update hooks by using function, as in my reply, to avoid a lot of later trouble you might meet :)
But, in my opinion, it would be better to update the hook from the children, I'm not sure if you need ref for anything, since you are new to react, I would believe you do not. If you don't need it, then, pass the setTestHeight to children, update it how I showed your, and then you will get all your height, and from the parent, by seeing when your array is full, you will be able to determine your maxheight (that you should store in another hook) and then, update all your children max height
I'm also not sure why your are using forwardref though.

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));
}

Delay state change with async/await?

I'm making a custom error window that pops up in various situations. What I'm struggling with is getting the window to dissapear after 2 seconds.. Just a simple setTimeout to change the popup state to active:false is a bit unreliable because of the way the even loop works (i think?).
So I'm attempting an async/await way of doing it making sure it's always exactly 2 seconds. However the way I have done it below the timing still seems to be very weird, sometimes instant, sometimes 2 seconds.
How do I get my removeErrorMsg function to wait 2 seconds before setting the state?
///// App.js.js ////
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
export default class App extends Component {
state = {
errorPopup: {
active: false,
message: ''
}
}
removeErrorMsg = async() => {
await delay(2000);
this.setState({errorPopup: {active: false, message: ''}});
}
}
///// ErrorPopup.js ////
import React from 'react'
const ErrorPopup = ({ message, active, removeErrorMsg}) => {
if(active){
removeErrorMsg()
return (
<div className="error-popup">
<p>{message}</p>
</div>
)
} else return <div></div>
}
export default ErrorPopup
You must call the removeErrorMsg inside the ErrorPopup component within a useEffect function. Directly calling it will result in another delay being created which resets the state as soon as any other action in parent component tries to trigger a re-render leading to unexpected behaviours
const ErrorPopup = ({ message, active, removeErrorMsg}) => {
useEffect(() => {
if(active) {
removeErrorMsg()
}
}, [active])
if(active){
return (
<div className="error-popup">
<p>{message}</p>
</div>
)
} else return <div></div>
}
P.S. Although there is no gurantee that the setTimeout will execute immediately at 2sec, more or less it roughly execute around 2sec.

React useCallback does not get updated on state change

The sample below is a simplified excerpt where a child component emits events based on mouse behaviours. React then should update the DOM according to the emitted events.
function SimpleSample() {
const [addons, setAddons] = React.useState<any>({
google: new GoogleMapsTile('google'),
})
const [tooltip, setTooltip] = React.useState<null | { text: string[]; x; y }>(null)
React.useEffect(() => {
// ...
}, [])
const mapEventHandle = React.useCallback(
(event: MapEvents) => {
console.log('event', event.type, tooltip) // LOG 1
if (event.type === MapEventType.mouseoverPopup_show) {
setTooltip({ text: event.text, x: event.coordinates[0], y: event.coordinates[1] })
} else if (event.type === MapEventType.mouseoverPopup_move) {
if (tooltip) setTooltip({ ...tooltip, x: event.coordinates[0], y: event.coordinates[1] })
} else if (event.type === MapEventType.mouseoverPopup_hide) {
setTooltip(null)
}
},
[tooltip]
)
console.log('render', tooltip) // LOG 2
return <MapComponent addons={addons} onEvent={mapEventHandle} />
}
The following order of events is expected:
mouseoverPopup_show is emitted, then tooltip changed from null to a value, a rerender occurs
mouseoverPopup_move is emitted, then tooltip is updated, triggering a rerender
What actually is happening:
Logpoint LOG 2 logs the updated value of tooltip (correct)
When mapEventHandle is called again, the value of tooltip inside that closure (logpoint LOG 1) is never updated, being always null.
Am I missing somethinig? Using the wrong hook?
Here's a codesandbox for it
https://codesandbox.io/s/blissful-torvalds-wm27f
EDIT: On de codesandbox sample setTooltip is not even triggering a rerender
Thanks for the help folks, the issue seems to be down inside a dependency of <MapComponent/>. It ended up saving a reference to the old callback on construction. Still a caveat to watch for, and which i probably wouldnt face with class components...
//something like this
class MapComponent {
emitter = this.props.onChange //BAD
emitter = (...i) => this.props.onChange(...i) //mmkay
}
I think event.coordinates is undefined so event.coordinates[0] causes an error.
If you do: setTooltip({working:'fine'}); you'll get type errors but it does set the toolTip state and re renders.
Thanks to your answer it helped me debug mine which was a bit different. Mine was not working because the callback reference was kept in a state value of the child component.
const onElementAdd = useCallBack(...)..
<Dropzone onElementAdded={props.onElementAdded} />
export const Dropzone = (props: DropzoneProps): JSX.Element => {
const [{ isOver }, drop] = useDrop(
() => ({
accept: props.acceptedTypes,
drop: (item: BuilderWidget, monitor): void => {
if (monitor.didDrop()) return;
props.onElementAdded(item);
},
}),
// missed props.onElementAdded here
[props.onElementAdded, props.acceptedTypes, props.disabled],
);

Resources