I'm trying to create a library to generate skeleton-loading components on the fly. I like those skeletons components with a shimmer effect, but I don't want to create them manually. So my idea is something like this:
I create a component which receives the component that I want to load as a children
while loading, I render a copy of this component, but invisible
I iterate through the component elements, and render a skeleton component based on that
I've already made some progress so far, I created a nextjs boilerplate just for testing purposes, I'm new at opensource, and I will create a library with typescript and rollup when I'm done testing.
My code by now is in this repository: https://github.com/FilipePfluck/skeleton-lib-test
here is the core component:
const Shimmer = ({ children, isLoading, component: Component, exampleProps }) => {
const fakeComponentRef = useRef(null)
useEffect(()=>{
console.log(fakeComponentRef.current.children)
},[fakeComponentRef])
const texts = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'strong']
const contents = ['img', 'video', 'button']
const styleProps = ['borderRadius', 'padding', 'margin', 'marginRight', 'marginLeft', 'marginTop', 'marginBottom', 'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight', 'display', 'alignItems', 'justifyContent', 'flexDirection']
const renderElement = (element) => {
console.log('renderElement')
const object = {}
styleProps.forEach(s => Object.assign(object, {[s]: element.style[s]}))
if(texts.includes(element.localName)){
const fontSize = +document.defaultView.getComputedStyle(element, null)["fontSize"].replace('px','')
const lineHeight = +document.defaultView.getComputedStyle(element, null)["lineHeight"].replace('px','') | fontSize * 1.2
const numberOfLines = Math.round(element.offsetHeight / lineHeight)
const lineMarginBottom = lineHeight - fontSize
const lines = []
for(let i=0; i<numberOfLines; i++){
lines.push(i)
}
return(
<div style={{display: 'flex', flexDirection: 'column'}}>
{lines.map(line => (
<div
style={{
width: element.offsetWidth,
...object,
height: fontSize,
marginBottom: lineMarginBottom
}}
className="shimmer"
key={"line"+line}
/>))}
</div>
)
}
if(contents.includes(element.localName)){
return (
<div
style={{
width: element.offsetWidth,
height: element.offsetHeight,
...object
}}
className={'shimmer'}
/>
)
}
return (
<div
style={{
width: element.offsetWidth,
height: element.offsetHeight,
display: element.style.display,
alignItems: element.style.alignItems,
justifyContent: element.style.justifyContent,
flexDirection: element.style.flexDirection,
padding: element.style.padding,
margin: element.style.margin
}}
>
{!!element.children
? [...element.children]
.map(child => renderElement(child))
: null
}
</div>
)
}
return isLoading ? (
<>
<div style={{visibility: 'hidden', position: 'absolute'}} ref={fakeComponentRef}>
<Component {...exampleProps}/>
</div>
{fakeComponentRef?.current && renderElement(fakeComponentRef.current)}
</>
) : children
}
Basically I'm doing a recursive function to render the skeleton component. But I got a problem: the loading component is taking too long to render. Sometimes it takes over 40 seconds. And this is a massive problem, a loading component should not take longer to render then the loading itself. But I have no clue why is it taking so long. Also, I know that there is a lot of other problems, but my first goal was to render the component on the screen. I'd be pleased if any of you wanted to contribute to my project
UPDATE:
I kinda solved it. I commented the piece of code where the function calls itself, and it took the same time to render. So, I concluded something was interrupting the code during the first execution or before. I guess the problem was the fakeComponentRef?.current && before the function call. without this validation it wouldn't work because the ref starts as null, but I guess the UI doesn't change when a ref changes, as it would because of a state? Then I created a useEffect with a setTimeout to wait 1ms (just to make sure it is not null) to render the component and it worked.
Related
i have this function component as part of my project. i haven't called any hook inside loops, conditions, or nested functions as stated in the react document. i also have placed my hooks before any return statement so that react can reach each of them. yet i get this error which says Uncaught Error: Rendered fewer hooks than expected. This may be caused by an accidental early return statement.
any idea what the problem is ?
function ScrollRenderingCom(props) {
const { children, aproximateHeight = 200} = props;
const [isIntersecting, setIsIntersecting] = useState(false);
const fakeComponent = useRef(null);
const callback = useCallback((entries) => {
const [entry] = entries;
if (entry.isIntersecting) {
setIsIntersecting(true)
}
}, []);
useEffect(() => {
const options = {
root: null,
rootMargin: '0px',
threshold: 0,
}
const observer = new IntersectionObserver(callback, options);
observer.observe(fakeComponent.current);
return () => {
observer.disconnect();
}
}, [callback]);
return (
isIntersecting ? children
:
<div
ref={fakeComponent}
className={children.type().props.className}
style={{ visibility: 'hidden',
minHeight: aproximateHeight + 'px' }}
>
</div>
)
}
and this is the component stack trace that react prints to the console which specifies the error occurred in this component
The above error occurred in the ScrollRenderingCom component:
at ScrollRenderingCom (http://localhost:3000/static/js/bundle.js:3468:5)
at div
at PostSection1
at Home
at Routes (http://localhost:3000/static/js/bundle.js:229775:5)
at div
at Router (http://localhost:3000/static/js/bundle.js:229708:15)
at BrowserRouter (http://localhost:3000/static/js/bundle.js:228517:5)
at App
Update:
from negative reflections, i noticed that my sample code is insufficient to reproduce the issue. hence i provide the above component with a single Child component so that it can be run and inspected in isolation.
here is the Child component
function Child() {
const [successMessage, setSuccessMessage] = useState(false);
useEffect(() => {
if (successMessage) {
alert('this app works correctly')
}
return () => {setSuccessMessage(false)}
})
const handleOnClick = () => {
setSuccessMessage(true);
}
return(
<div className="child"
style={{width: '500px',
height: '200px',
backgroundColor: 'green',
color: 'white'
}}
>
<h1>this component should be rendered when
the empty blue div element completely
enters the viewport
</h1>
<h3>if you see the above message click Yes</h3>
<button onClick={handleOnClick} >Yes</button>
</div>
)
}
and here is the final live demo that you can inspect the problem.
please let me know of any further deficiencies in my question and give me a chance to fix them before downing me
By calling children.type you create that component and that's causing hooks to be run conditionally.
Try changing:
children.type().props.className
to
children.props.className
No need to create the component here
https://codesandbox.io/s/demo-forked-egofes?file=/src/ScrollRenderingCom.js:816-825
When I change something on my page such as checking radio buttons or switching tabs, new network requests to retrieve images are sent from the browser. I've noticed this with a couple of websites I've made, but there should never be another request; I'm not performing a fetch on changing these values in the frontend. I don't see why the images should be requested again.
I've attached a gif showing it happening in an app I'm making with React Native, although I've seen it in my React projects too. You can see the images flicker as I switch tabs and the network calls in devtools on the right, and I'm also worried about the performance impact.
How can I prevent this from happening?
For context the data flow in my app is as follows:
In App.tsx render MainStackNavigator component.
In MainStackNavigator call firebase to retrieve data (including images). Store that data in Context.
In Home.tsx render the Tabs component, but also create an array containing the tabs data, namely the name of the component to render and the component itself.
In tabs render content based on selected tab.
Home.tsx
export const Home = (): ReactNode => {
const scenes = [
{
key: "first",
component: EnglishBreakfastHome,
},
{
key: "second",
component: SecondRoute,
},
];
return (
<View flex={1}>
<Tabs scenes={scenes} />
</View>
);
};
Tabs.tsx
export const Tabs = ({ scenes }: TabsProps): ReactNode => {
const renderScenes = useMemo(() =>
scenes.reduce((map, scene) => {
map[scene.key] = scene.component;
return map;
}, {})
);
const renderScene = SceneMap(renderScenes);
const [index, setIndex] = useState(0);
const [routes] = useState([
{ key: "first", title: "Breakfast" },
{ key: "second", title: "Herbal" },
]);
const renderTabBar = ({ navigationState, position }: TabViewProps) => {
const inputRange = navigationState.routes.map((_, i) => i);
return (
<Box flexDirection="row">
{navigationState.routes.map((route, i) => {
const opacity = position.interpolate({
inputRange,
outputRange: inputRange.map((inputIndex) =>
inputIndex === i ? 1 : 0.5
),
});
return (
// Tab boxes and styling
);
})}
</Box>
);
};
return (
<TabView // using react-native-tab-view
style={{ flexBasis: 0 }}
navigationState={{ index, routes }}
renderScene={renderScene}
renderTabBar={renderTabBar}
onIndexChange={setIndex}
initialLayout={{ width: layout.width }}
/>
);
};
Look into something like react-native-fast-image. It is even recommended by the react-native docs.
It handles cashing of images. From the docs:
const YourImage = () => (
<FastImage
style={{ width: 200, height: 200 }}
source={{
uri: 'https://unsplash.it/400/400?image=1',
headers: { Authorization: 'someAuthToken' },
priority: FastImage.priority.normal,
}}
resizeMode={FastImage.resizeMode.contain}
/>
)
The problem here wasn't image caching at all, it was state management.
I made a call to a db to retrieve images, and then stored that array of images in AppContext which wrapped the whole app. Then, everytime I changed state at all, I ran into this problem because the app was being re-rendered. There were two things I did to remove this problem:
Separate Context into separate stores rather than just using a single global state object. I created a ContentContext that contained all the images so that state was kept separate from other state changes and, therefore, re-renders weren't triggered.
I made use of useMemo, and re-wrote the cards (that you can see in the image) to only change when the main images data array changed. This means that, regardless of changes to other state variables, that component will not need to re-render until the images array changes.
Either of the two solutions should work on their own, but I used both just to be safe.
I am relatively new to React-JS and was wondering how I could pass my variables to my export function. I am using the jsPDF library.
At the time the Summary page is showing up, every thing is already in the database.
The Summary page creates in every round an IdeaTable component, writes it into an array and renders it bit by bit if the users click on the Next button (showNextTable()).
This component can use a JoinCode & playerID to assemble the table that was initiated by this player.
import React, { Component } from "react";
import { connect } from "react-redux";
import { Box, Button } from "grommet";
import IdeaTable from "../playerView/subPages/ideaComponents/IdeaTable";
import QuestionBox from "./QuestionBox";
import { FormUpload } from 'grommet-icons';
import jsPDF from 'jspdf';
export class Summary extends Component {
state = {
shownTable: 0
};
showSummary = () => {};
showNextTable = () => {
const { players } = this.props;
const { shownTable } = this.state;
this.setState({
shownTable: (shownTable + 1) % players.length
});
};
exportPDF = () => {
var doc = new jsPDF('p', 'pt');
doc.text(20,20, " Test string ");
doc.setFont('courier');
doc.setFontType('bold');
doc.save("generated.pdf");
};
render() {
const { topic, players } = this.props;
const { shownTable } = this.state;
const tables = [];
for (let i = 0; i < players.length; i++) {
const player = players[i];
const table = (
<Box pad={{ vertical: "large", horizontal: "medium" }}>
<IdeaTable authorID={player.id} />
</Box>
);
tables.push(table);
}
return (
<Box
style={{ wordWrap: "break-word" }}
direction="column"
gap="medium"
pad="small"
overflow={{ horizontal: "auto" }}
>
<QuestionBox question={topic} />
{tables[shownTable]}
<Button
primary
hoverIndicator="true"
style={{ width: "100%" }}
onClick={this.showNextTable}
label="Next"
/>
< br />
<Button
icon={ <FormUpload color="white"/> }
primary={true}
hoverIndicator="true"
style={{
width: "30%",
background: "red",
alignSelf: "center"
}}
onClick={this.exportPDF}
label="Export PDF"
/>
</Box>
);
}
}
const mapStateToProps = state => ({
topic: state.topicReducer.topic,
players: state.topicReducer.players
});
const mapDispatchToProps = null;
export default connect(mapStateToProps, mapDispatchToProps)(Summary);
So basically how could I include the IdeaTable to work with my pdf export?
If you want to use html module of jsPDF you'll need a reference to generated DOM node.
See Refs and the DOM on how to get those.
Alternatively, if you want to construct PDF yourself, you would use data (e.g. from state or props), not the component references.
Related side note:
On each render of the parent component you are creating new instances for all possible IdeaTable in a for loop, and all are the same, and most not used. Idiomatically, this would be better:
state = {
shownPlayer: 0
};
Instead of {tables[shownTable]} you would have:
<Box pad={{ vertical: "large", horizontal: "medium" }}>
<IdeaTable authorID={shownPlayer} ref={ideaTableRef}/>
</Box>
And you get rid of the for loop.
This way, in case you use html dom, you only have one reference to DOM to store.
In case you decide to use data to generate pdf on your own, you just use this.props.players[this.state.shownPlayer]
In case you want to generate pdf for all IdeaTables, even the ones not shown, than you can't use API that needs DOM. You can still use your players props to generate your own PDF, or you can consider something like React-Pdf
I'm trying to do a memoize Modal and I have a problem here.
When I change input I dont need to re-render the Modal component.
For example:
Modal.tsx looks like this:
import React from "react";
import { StyledModalContent, StyledModalWrapper, AbsoluteCenter } from "../../css";
interface ModalProps {
open: boolean;
onClose: () => void;
children: React.ReactNode
};
const ModalView: React.FC<ModalProps> = ({ open, onClose, children }) => {
console.log("modal rendered");
return (
<StyledModalWrapper style={{ textAlign: "center", display: open ? "block" : "none" }}>
<AbsoluteCenter>
<StyledModalContent>
<button
style={{
position: "absolute",
cursor: "pointer",
top: -10,
right: -10,
width: 40,
height: 40,
border: 'none',
boxShadow: '0 10px 10px 0 rgba(0, 0, 0, 0.07)',
backgroundColor: '#ffffff',
borderRadius: 20,
color: '#ba3c4d',
fontSize: 18
}}
onClick={onClose}
>
X
</button>
{open && children}
</StyledModalContent>
</AbsoluteCenter>
</StyledModalWrapper>
);
};
export default React.memo(ModalView);
Here is an example of how I wrap it.
import React from 'react'
import Modal from './modal';
const App: React.FC<any> = (props: any) => {
const [test, setTest] = React.useState("");
const [openCreateChannelDialog, setOpenCreateChannelDialog] = React.useState(false);
const hideCreateModalDialog = React.useCallback(() => {
setOpenCreateChannelDialog(false);
}, []);
return (
<>
<input type="text" value={test} onChange={(e) => setTest(e.target.value)} />
<button onClick={() => setOpenCreateChannelDialog(true)}>Create channel</button>
<Modal
open={openCreateChannelDialog}
onClose={hideCreateModalDialog}
children={<CreateChannel onClose={hideCreateModalDialog} />}
/>
</>
};
I know, Modal re-rendered because children reference created every time when App component re-renders (when I change an input text).
Know I'm interested, if I wrap <CreateChannel onClose={hideCreateModalDialog} /> inside React.useMemo() hook
For example:
const MemoizedCreateChannel = React.useMemo(() => {
return <CreateChannel onClose={hideCreateModalDialog} />
}, [hideCreateModalDialog]);
And change children props inside Modal
from:
children={<CreateChannel onClose={hideCreateModalDialog} />}
to
children={MemoizedCreateChannel}
It works fine, but is it safe? And it is only one solution that tried to memoize a Modal?
Memoizing JSX expressions is part of the official useMemo API:
const Parent = ({ a }) => useMemo(() => <Child1 a={a} />, [a]);
// This is perfectly fine; Child re-renders only, if `a` changes
useMemo memoizes individual children and computed values, given any dependencies. You can think of memo as a shortcut of useMemo for the whole component, that compares all props.
But memo has one flaw - it doesn't work with children:
const Modal = React.memo(ModalView);
// React.memo won't prevent any re-renders here
<Modal>
<CreateChannel />
</Modal>
children are part of the props. And React.createElement always creates a new immutable object reference (REPL). So each time memo compares props, it will determine that children reference has changed, if not a primitive.
To prevent this, you can either use useMemo in parent App to memoize children (which you already did). Or define a custom comparison function for memo, so Modal component now becomes responsible for performance optimization itself. react-fast-compare is a handy library to avoid boiler plate for areEqual.
Is it safe? Yes. At the end of the day the JSX is just converted into a JSON object, which is totally fine to memoize.
That said, I think it is stylistically a bit weird to do this, and I could foresee it leading to unexpected bugs in the future if things need to change and you don't think it through fully.
I am trying to hide the the card once the time is equal with 0 but the timeLeft is never updated. Any idea what am I doing wrong in here?
<div style={{ flexDirection: "column", overflowY: "scroll" }}>
{sorted.map((d, i) => {
let timeLeft;
return (
<div key={i} className="card__container">
<ProgressBar
percentage={d.time * 10}
timing={res => (timeLeft = res)}
/>
</div>
);
})}
</div>
My ProgressBar looks like this
constructor(props) {
super(props);
this.state = {
timeRemainingInSeconds: props.percentage
};
}
componentDidMount() {
timer = setInterval(() => {
this.decrementTimeRemaining();
}, 1000);
}
decrementTimeRemaining = () => {
if (this.state.timeRemainingInSeconds > 0) {
this.setState({
timeRemainingInSeconds: this.state.timeRemainingInSeconds - 10
});
} else {
clearInterval(timer);
this.props.timing(0);
}
};
render() {
return (
<div className="Progress_wrapper">
<div
className="Progress_filler"
style={{ width: `${this.state.timeRemainingInSeconds / 2}%` }}
/>
</div>
);
}
}
I think the variable doesn't get updated, because your map function is kind of a functional component, which therefore is a render function. The variable is basically always reset if you make changes to your parent component.
You could create a real functional component by externalizing it and using a useState hook, kind of the following (I really don't know, but this could also work as definition inside your map function):
function CardContainer(props) {
const [timeLeft, setTimeLeft] = useState(0); // #todo set initial state based on your own definition
return (
<div key={i} className="card__container">
<ProgressBar
percentage={d.time * 10}
timing={res => setTimeLeft(res)}
/>
</div>
);
}
It would be easier if you can reference the library that you are using for ProgressBar. If it is not from a library, reference the source code for it.
I see some issues with the timing prop here.
<ProgressBar
percentage={d.time * 10}
timing={res => (timeLeft = res)}
/>
My understanding of your props for ProgressBar
Timing
This should be a static value that the component would just display as a visual indicator. We have to take note of the required units for this prop.
Percentage
This should be a value from 0 to 100 that will decide how the progress bar will visually look like.
Your question: Hide the component when timing is zero(or perhaps when percentage is at 100?).
For this, we can deploy the strategy to manipulate the style of the div that is wrapping this component.
<div style={{ flexDirection: "column", overflowY: "scroll" }}>
{sorted.map((d, i) => {
const percentage = d.time * 10; // this is taken from your own computation of percentage. I reckon that you need to divide by 100.
let customStyle = {};
if(percentage === 100){
customStyle = {
display: "none"
}
}
return (
<div key={i} className="card__container" style={customStyle}>
<ProgressBar
percentage={percentage}
timing={res => (timeLeft = res)}
/>
</div>
);
})}
</div>