I have the following code, which should render a simple fade in animation for the Container component after 3 seconds. However, the component is flashing fully visible before fading in. My question is: why is this happening, and how can I stop it from happening?
import React, { useState, useEffect } from "react";
import { render } from "react-dom";
import posed, { PoseGroup } from "react-pose";
import styled from "styled-components";
const sequence = b =>
b.every(
(a, i) => !(a.call ? a() : setTimeout(() => sequence(b.slice(++i)), a))
);
const usePose = (initial, poses = {}) => {
const [pose, setPose] = useState(initial);
return { pose, setPose, poses };
};
const useAnimation = () => {
const { pose, setPose } = usePose(`hidden`, [`hidden`, `normal`]);
useEffect(() => {
sequence([3000, () => setPose(`normal`)]);
}, []);
return {
pose
};
};
const Container = styled(
posed.div({
hidden: {
opacity: 0
},
normal: { opacity: 1 }
})
)({
color: "red"
});
const App = () => {
const { pose } = useAnimation();
return (
<PoseGroup animateOnMount>
<Container key={0} pose={pose}>
<h1>hello world</h1>
</Container>
</PoseGroup>
);
};
const rootElement = document.getElementById("root");
render(<App />, rootElement);
Issue solved by:
const Container = styled(
posed.div({
hidden: {
opacity: 0
},
normal: { opacity: 1 }
})
)({
color: "red"
opacity: 0, // Add this to stop flash.
});
Related
I have a componenet that wraps its children and slides them in and out based on the stage prop, which represents the active child's index.
As this uses a .map() to wrap each child in a div for styling, I need to give each child a key prop. I want to assign a random key as the children could be anything.
I thought I could just do this
key={`pageSlide-${uuid()}`}
but it causes an infinite loop/React to freeze and I can't figure out why
I have tried
Mapping the children before render and adding a uuid key there, calling it via key={child.uuid}
Creating an array of uuids and assigning them via key={uuids[i]}
Using a custom hook to store the children in a state and assign a uuid prop there
All result in the same issue
Currently I'm just using the child's index as a key key={pageSlide-${i}} which works but is not best practice and I want to learn why this is happening.
I can also assign the key directly to the child in the parent component and then use child.key but this kinda defeats the point of generating the key
(uuid is a function from react-uuid, but the same issue happens with any function including Math.random())
Here is the full component:
import {
Children,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import PropTypes from "prop-types";
import uuid from "react-uuid";
import ProgressBarWithTicks from "./ProgressBarWithTicks";
import { childrenPropType } from "../../../propTypes/childrenPropTypes";
const calculateTranslateX = (i = 0, stage = 0) => {
let translateX = stage === i ? 0 : 100;
if (i < stage) {
translateX = -100;
}
return translateX;
};
const ComponentSlider = ({ stage, children, stageCounter }) => {
const childComponents = Children.toArray(children);
const containerRef = useRef(null);
const [lastResize, setLastResize] = useState(null);
const [currentMaxHeight, setCurrentMaxHeight] = useState(
containerRef.current?.childNodes?.[stage]?.clientHeight
);
const updateMaxHeight = useCallback(
(scrollToTop = true) => {
if (scrollToTop) {
window.scrollTo(0, 0);
}
setCurrentMaxHeight(
Math.max(
containerRef.current?.childNodes?.[stage]?.clientHeight,
window.innerHeight -
(containerRef?.current?.offsetTop || 0) -
48
)
);
},
[stage]
);
useEffect(updateMaxHeight, [stage, updateMaxHeight]);
useEffect(() => updateMaxHeight(false), [lastResize, updateMaxHeight]);
const resizeListener = useMemo(
() => new MutationObserver(() => setLastResize(Date.now())),
[]
);
useEffect(() => {
if (containerRef.current) {
resizeListener.observe(containerRef.current, {
childList: true,
subtree: true,
});
}
}, [resizeListener]);
return (
<div className="w-100">
{stageCounter && (
<ProgressBarWithTicks
currentStage={stage}
stages={childComponents.length}
/>
)}
<div
className="position-relative divSlider align-items-start"
ref={containerRef}
style={{
maxHeight: currentMaxHeight || null,
}}>
{Children.map(childComponents, (child, i) => (
<div
key={`pageSlide-${uuid()}`}
className={`w-100 ${
stage === i ? "opacity-100" : "opacity-0"
} justify-content-center d-flex`}
style={{
zIndex: childComponents.length - i,
transform: `translateX(${calculateTranslateX(
i,
stage
)}%)`,
pointerEvents: stage === i ? null : "none",
cursor: stage === i ? null : "none",
}}>
{child}
</div>
))}
</div>
</div>
);
};
ComponentSlider.propTypes = {
children: childrenPropType.isRequired,
stage: PropTypes.number,
stageCounter: PropTypes.bool,
};
ComponentSlider.defaultProps = {
stage: 0,
stageCounter: false,
};
export default ComponentSlider;
It is only called in this component (twice, happens in both instances)
import { useEffect, useReducer, useState } from "react";
import { useParams } from "react-router-dom";
import {
FaCalendarCheck,
FaCalendarPlus,
FaHandHoldingHeart,
} from "react-icons/fa";
import { IoIosCart } from "react-icons/io";
import { mockMatches } from "../../../templates/mockData";
import { initialSwapFormState } from "../../../templates/initalStates";
import swapReducer from "../../../reducers/swapReducer";
import useFetch from "../../../hooks/useFetch";
import useValidateFields from "../../../hooks/useValidateFields";
import IconWrap from "../../common/IconWrap";
import ComponentSlider from "../../common/transitions/ComponentSlider";
import ConfirmNewSwap from "./ConfirmSwap";
import SwapFormWrapper from "./SwapFormWrapper";
import MatchSwap from "../Matches/MatchSwap";
import SwapOffers from "./SwapOffers";
import CreateNewSwap from "./CreateNewSwap";
import smallNumberToWord from "../../../functions/utils/numberToWord";
import ComponentFader from "../../common/transitions/ComponentFader";
const formStageHeaders = [
"What shift do you want to swap?",
"What shifts can you do instead?",
"Pick a matching shift",
"Good to go!",
];
const NewSwap = () => {
const { swapIdParam } = useParams();
const [formStage, setFormStage] = useState(0);
const [swapId, setSwapId] = useState(swapIdParam || null);
const [newSwap, dispatchNewSwap] = useReducer(swapReducer, {
...initialSwapFormState,
});
const [matches, setMatches] = useState(mockMatches);
const [selectedMatch, setSelectedMatch] = useState(null);
const [validateHook, newSwapValidationErrors] = useValidateFields(newSwap);
const fetchHook = useFetch();
const setStage = (stageIndex) => {
if (!swapId && stageIndex > 1) {
setSwapId(Math.round(Math.random() * 100));
}
if (stageIndex === "reset") {
setSwapId(null);
dispatchNewSwap({ type: "reset" });
}
setFormStage(stageIndex === "reset" ? 0 : stageIndex);
};
const saveMatch = async () => {
const matchResponse = await fetchHook({
type: "addSwap",
options: { body: newSwap },
});
if (matchResponse.success) {
setStage(3);
} else {
setMatches([]);
dispatchNewSwap({ type: "setSwapMatch" });
setStage(1);
}
};
useEffect(() => {
// set matchId of new selected swap
dispatchNewSwap({ type: "setSwapMatch", payload: selectedMatch });
}, [selectedMatch]);
return (
<div>
<div className="my-3">
<div className="d-flex justify-content-center w-100 my-3">
<ComponentSlider stage={formStage}>
<IconWrap colour="primary">
<FaCalendarPlus />
</IconWrap>
<IconWrap colour="danger">
<FaHandHoldingHeart />
</IconWrap>
<IconWrap colour="warning">
<IoIosCart />
</IconWrap>
<IconWrap colour="success">
<FaCalendarCheck />
</IconWrap>
</ComponentSlider>
</div>
<ComponentFader stage={formStage}>
{formStageHeaders.map((x) => (
<h3
key={`stageHeading-${x.id}`}
className="text-center my-3">
{x}
</h3>
))}
</ComponentFader>
</div>
<div className="mx-auto" style={{ maxWidth: "400px" }}>
<ComponentSlider stage={formStage} stageCounter>
<SwapFormWrapper heading="Shift details">
<CreateNewSwap
setSwapId={setSwapId}
newSwap={newSwap}
newSwapValidationErrors={newSwapValidationErrors}
dispatchNewSwap={dispatchNewSwap}
validateFunction={validateHook}
setStage={setStage}
/>
</SwapFormWrapper>
<SwapFormWrapper heading="Swap in return offers">
<p>
You can add up to{" "}
{smallNumberToWord(5).toLowerCase()} offers, and
must have at least one
</p>
<SwapOffers
swapId={swapId}
setStage={setStage}
newSwap={newSwap}
dispatchNewSwap={dispatchNewSwap}
setMatches={setMatches}
/>
</SwapFormWrapper>
<SwapFormWrapper>
<MatchSwap
swapId={swapId}
setStage={setStage}
matches={matches}
selectedMatch={selectedMatch}
setSelectedMatch={setSelectedMatch}
dispatchNewSwap={dispatchNewSwap}
saveMatch={saveMatch}
/>
</SwapFormWrapper>
<SwapFormWrapper>
<ConfirmNewSwap
swapId={swapId}
setStage={setStage}
selectedSwap={selectedMatch}
newSwap={newSwap}
/>
</SwapFormWrapper>
</ComponentSlider>
</div>
</div>
);
};
NewSwap.propTypes = {};
export default NewSwap;
One solution
#Nick Parsons has pointed out I don't even need a key if using React.Children.map(), so this is a non issue
I'd still really like to understand what was causing this problem, aas far as I can tell updateMaxHeight is involved, but I can't quite see the chain that leads to an constant re-rendering
Interstingly if I use useMemo for an array of uuids it works
const uuids = useMemo(
() => Array.from({ length: childComponents.length }).map(() => uuid()),
[childComponents.length]
);
/*...*/
key={uuids[i]}
I'm using NextJS and Remirror Editor plugin. I get an error that setContent is undefined on the index page where the editor is loaded. I want to add an "external" button after the Component is loaded to exit the text. The component is dynamically externally loaded. I'm really unsure how to make the external button/text change to work.
Index.tsx:
import { NextPage, GetStaticProps } from 'next';
import dynamic from 'next/dynamic';
import { WysiwygEditor } from '#remirror/react-editors/wysiwyg';
import React, { useRef, useEffect } from 'react';
const TextEditor = dynamic( () => import('../text-editor'), { ssr: false } );
import natural from "natural";
export interface EditorRef {
setContent: (content: any) => void;
}
type Props = {
aaa:string
bbb:string
}
const Home: NextPage< Props > = (props) => {
//hook call ref to use editor externally
const ref = useRef<EditorRef | null>(null);
const {aaa,bbb} = props;
return (
<>
<ul>
{aaa} </ul>
Next.js Home Page
<TextEditor ref={ref} />
{
useEffect(()=>{
ref.current!.setContent({content: "testing the text has changed"})
},[])}
<button onClick={() => ref.current.setContent({content: "testing the text has changed"})}>Set another text button 2</button>
</>
);
};
var stringWordNet = "";
export const getStaticProps = async ():Promise<GetStaticPropsResult<Props>> => {
var wordnet = new natural.WordNet();
wordnet.lookup('node', function(results) {
stringWordNet = String(results[0].synonyms[1]);
});
return {
props:{
aaa:stringWordNet,
bbb:"bbb"
}
}
};
export default Home;
text-editor.tsx
import 'remirror/styles/all.css';
import { useEffect, useState, forwardRef, Ref, useImperativeHandle, useRef } from 'react';
import { BoldExtension,
ItalicExtension,
selectionPositioner,
UnderlineExtension, MentionAtomExtension } from 'remirror/extensions';
import { cx } from '#remirror/core';
import {
EditorComponent,
FloatingWrapper,
MentionAtomNodeAttributes,
Remirror,
useMentionAtom,
useRemirror, ThemeProvider, useRemirrorContext
} from '#remirror/react';
import { css } from "#emotion/css";
const styles = css`
background-color: white;
color: #101010;
border: 1px solid #ccc;
border-radius: 4px;
padding: 4px;
`;
const ALL_USERS = [
{ id: 'wordnetSuggestion1', label: 'NotreSuggestion1' },
{ id: 'wordnetSuggestion2', label: 'NotreSuggestion2' },
{ id: 'wordnetSuggestion3', label: 'NotreSuggestion3' },
{ id: 'wordnetSuggestion4', label: 'NotreSuggestion4' },
{ id: 'wordnetSuggestion5', label: 'NotreSuggestion5' },
];
const MentionSuggestor: React.FC = () => {
return (
<FloatingWrapper positioner={selectionPositioner} placement='bottom-start'>
<div>
{
ALL_USERS.map((option, index) => (
<li> {option.label}</li>
))
}
</div>
</FloatingWrapper>
);
};
const DOC = {
type: 'doc',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'New content',
},
],
},
],
};
//Start Imperative Handle Here
export interface EditorRef {
setContent: (content: any) => void;
}
const ImperativeHandle = forwardRef((_: unknown, ref: Ref<EditorRef>) => {
const { setContent } = useRemirrorContext({
autoUpdate: true,
});
// Expose content handling to outside
useImperativeHandle(ref, () => ({ setContent }));
return <></>;
});
//Make content to show.
const TextEditor = forwardRef((_: unknown, ref: Ref<EditorRef>) => {
const editorRef = useRef<EditorRef | null>(null);
const { manager, state, getContext } = useRemirror({
extensions: () => [
new MentionAtomExtension()
],
content: '<p>I love Remirror</p>',
selection: 'start',
stringHandler: 'html',
});
return (
<>
<button
onMouseDown={(event) => event.preventDefault()}
onClick={() => editorRef.current!.setContent(DOC)}
></button>
<div className='remirror-theme'>
<Remirror manager={manager} initialContent={state}>
<EditorComponent />
<MentionSuggestor />
<ImperativeHandle ref={editorRef} />
</Remirror>
</div>
</>
);
});
export default TextEditor;
I have a big problem with React TimeLines Package(https://openbase.com/js/react-timelines)
I want something like this photo:
( having 3 P tags with different ClassNames)
but in default case of this package I cant do it!
I think I should use something like createElement and textContent in JS. but I dont know how!
My Codes:
import React, { Component } from "react";
import Timeline from "react-timelines";
import "react-timelines/lib/css/style.css";
import { START_YEAR, NUM_OF_YEARS, NUM_OF_TRACKS } from "./constant";
import { buildTimebar, buildTrack } from "./builder";
import { fill } from "./utils";
const now = new Date("2021-01-01");
const timebar = buildTimebar();
// eslint-disable-next-line no-alert
const clickElement = (element) =>
alert(`Clicked element\n${JSON.stringify(element, null, 2)}`);
class App extends Component {
constructor(props) {
super(props);
const tracksById = fill(NUM_OF_TRACKS).reduce((acc, i) => {
const track = buildTrack(i + 1);
acc[track.id] = track;
return acc;
}, {});
this.state = {
open: false,
zoom: 2,
// eslint-disable-next-line react/no-unused-state
tracksById,
tracks: Object.values(tracksById),
};
}
handleToggleOpen = () => {
this.setState(({ open }) => ({ open: !open }));
};
handleToggleTrackOpen = (track) => {
this.setState((state) => {
const tracksById = {
...state.tracksById,
[track.id]: {
...track,
isOpen: !track.isOpen,
},
};
return {
tracksById,
tracks: Object.values(tracksById),
};
});
};
render() {
const { open, zoom, tracks } = this.state;
const start = new Date(`${START_YEAR}`);
const end = new Date(`${START_YEAR + NUM_OF_YEARS}`);
return (
<div className="app">
<Timeline
scale={{
start,
end,
zoom,
}}
isOpen={open}
toggleOpen={this.handleToggleOpen}
clickElement={clickElement}
timebar={timebar}
tracks={tracks}
now={now}
enableSticky
scrollToNow
/>
</div>
);
}
}
export default App;
builder.js:
export const buildElement = ({ trackId, start, end, i }) => {
const bgColor = nextColor();
const color = colourIsLight(...hexToRgb(bgColor)) ? "#000000" : "#ffffff";
return {
id: `t-${trackId}-el-${i}`,
title: "Bye Title: Hello Type: String",
start,
end,
style: {
backgroundColor: `#${bgColor}`,
color,
borderRadius: "12px",
width: "auto",
height: "120px",
textTransform: "capitalize",
},
};
};
I have an issue with the AnimatePresence,used for page transition.
All work properly,except the fact when i load some pages (not all pages),the content of the page don't display,even if the url is correctly pushed,and i have to manually reload the page to see the content.
Here's my _app.js:
import '../assets/css/style.css';
import { AnimatePresence, motion } from 'framer-motion';
const pageVariants = {
pageInitial: {
backgroundColor: 'black',
opacity: 0
},
pageAnimate: {
backgroundColor: 'transparent',
opacity: 1
},
pageExit: {
backgroundColor: 'black',
opacity: 0
}
}
const pageMotionProps = {
initial: 'pageInitial',
animate: 'pageAnimate',
exit: 'pageExit',
variants: pageVariants
}
function handleExitComplete() {
if (typeof window !== 'undefined') {
window.scrollTo({ top: 0 })
}
}
const App = ({ Component, pageProps, router }) => {
return (
<AnimatePresence exitBeforeEnter onExitComplete={handleExitComplete}>
<motion.div key={router.route} {...pageMotionProps}>
<Component {...pageProps}/>
</motion.div>
</AnimatePresence>
)
export default App
And here the page,created with dinamic routing and a static approach:
import ReactMarkdown from 'react-markdown'
import { getArticles, getArticle, getCategories } from '../../lib/api'
import Layout from '../../components/layout/layout'
const Article = ({ article, categories }) => {
const imageUrl = article.image.url.startsWith('/')
? process.env.API_URL + article.image.url
: article.image.url
return (
<Layout categories={categories}>
<h1>{article.title}</h1>
<div>
<ReactMarkdown source={article.content} />
</div>
</Layout>
)
}
export async function getStaticPaths() {
const articles = (await getArticles()) || []
return {
paths: articles.map((article) => ({
params: {
id: article.id,
},
})),
fallback: false,
}
}
export async function getStaticProps({ params }) {
const article = (await getArticle(params.id)) || []
const categories = (await getCategories()) || []
return {
props: { article, categories },
unstable_revalidate: 1,
}
}
export default Article
Note: Some pages work perfectly,some pages need to be reloaded to show the content,and when loaded,the animation start without problems.
I don't know if it's important,but i'm fetching data with GraphQL
I'm using the https://github.com/locomotivemtl/locomotive-scroll plugin in react to enable smooth scrolling. I'm trying to get it to do a Continuous scroll so it will loop round no matter what direction you scroll in by adding and removing items in the state array to change the order.
This works but causes some super flicker.
coulep of demos is
https://codesandbox.io/s/heuristic-resonance-0ybkl
https://codesandbox.io/s/young-wind-ozeeq
https://codesandbox.io/s/patient-darkness-6gxj1
Any help would be appreciated
code below
import React from "react";
import ReactDOM from "react-dom";
import LocomotiveScroll from "locomotive-scroll";
import "./styles.css";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
limit: null,
items: [
"https://www.fillmurray.com/640/460",
"https://www.fillmurray.com/g/640/560",
"https://www.fillmurray.com/640/760",
"https://www.fillmurray.com/g/640/860",
"https://www.fillmurray.com/640/160",
"https://www.fillmurray.com/g/640/260",
"https://www.fillmurray.com/g/640/360",
"https://www.fillmurray.com/g/640/460"
]
};
}
componentDidMount() {
const target = document.querySelector("#view");
const scroll = new LocomotiveScroll({
el: target,
smooth: true,
smoothMobile: true,
getDirection: true
});
scroll.on("scroll", obj => {
console.log(obj.scroll.y);
if (this.state.limit !== null) {
if (obj.direction === "up") {
var current = this.state.items;
var first = current.shift();
current.push(first);
this.setState(
{
items: current
},
() => {
scroll.update();
}
);
}
if (obj.direction === "down") {
current = this.state.items;
var last = current.pop();
current.unshift(last);
this.setState(
{
items: current
},
() => {
scroll.update();
}
);
}
} else {
var halfway = obj.limit / 2;
scroll.scrollTo(target, halfway);
this.setState({
limit: obj.limit
});
}
});
scroll.scrollTo(target, 1);
}
render() {
return (
<div className="App">
<div id="view">
<div className="left">
{this.state.items.map(item => {
return <img src={item} alt="" />;
})}
</div>
</div>
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
The reason you are getting flickering is because the entire component is re-rendering on the scroll event and your elements don't have a key on them.
this.setState(
{
items: current
},
() => {
scroll.update();
}
);
In the above code, you set the state on a scroll event which will cause a full re-render of the list. You need to set a key on your elements that are in a map func.
Key docs
Code Sandbox
import React from "react";
import ReactDOM from "react-dom";
import LocomotiveScroll from "locomotive-scroll";
import "./styles.css";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
limit: null,
items: [
"https://www.fillmurray.com/640/460",
"https://www.fillmurray.com/g/640/560",
"https://www.fillmurray.com/640/760",
"https://www.fillmurray.com/g/640/860",
"https://www.fillmurray.com/640/160",
"https://www.fillmurray.com/g/640/260",
"https://www.fillmurray.com/g/640/360",
"https://www.fillmurray.com/g/640/460"
]
};
}
componentDidMount() {
const target = document.querySelector("#view");
const scroll = new LocomotiveScroll({
el: target,
smooth: true,
smoothMobile: true,
getDirection: true
});
scroll.on("scroll", ({ limit, direction }) => {
if (this.state.limit !== null) {
if (direction === "up") {
console.log("up");
var current = this.state.items;
var last = current.pop();
current.unshift(last);
this.setState(
{
items: current
},
() => {
scroll.update();
}
);
}
if (direction === "down") {
current = this.state.items;
var first = current.shift();
current.push(first);
this.setState(
{
items: current
},
() => {
scroll.update();
}
);
}
} else {
var halfway = limit / 2;
scroll.scrollTo(target, halfway);
this.setState({
limit
});
}
});
scroll.scrollTo(target, 1);
}
render() {
return (
<div className="App">
<div id="view">
<div className="left">
{this.state.items.map((item, idx) => {
return <img key={`${item}${idx}`} src={item} alt="" />;
})}
</div>
</div>
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);