How to prevent component to re-render every time? - javascript

I have a custom component where I want to prevent the useEffect to fire every time the component is rendering.
The main idea is to get the font-family name from the API and then pass it to the style value, so I want to get the font family just once - not every time the component renders in other screens.
Here's what I tried, but it doesn't work as expected, it's not updating the state after getting the value from API (getValue() not called).
import React, {useCallback, useEffect, useRef} from 'react';
import {useState} from 'react';
import {Text, StyleSheet, Platform} from 'react-native';
import {COLORS} from '../../common';
const AppText = ({children, style, ...rest}) => {
const isMounted = useRef(false);
const [fontFamily, setFontFamily] = useState('Helvetica-Bold');
const getValue = useCallback(() => {
// mock API
setTimeout(() => {
console.log('AppText: get font family name from API!!');
setFontFamily('HelveticaNeue');
}, 200);
}, []);
useEffect(() => {
if (isMounted.current) {
getValue();
} else {
isMounted.current = true;
return;
}
}, [getValue]);
return (
<Text
style={[
styles.text,
style,
{
fontFamily: fontFamily,
},
]}
{...rest}>
{children}
</Text>
);
};
export {AppText};
using:
Home/About/etc // other screens
const Home = () => {
return(
<View>
<AppText> Hey from home </AppText>
</View>
);
}

You can use a context to propergate the value down into multiple components (or use props) and just fetch it once higher up in the tree.
//App.js
import {useEffect, createContext, useState} from "react";
export const FontFamilyContext = createContext("DefaultFont");
const App = () =>{
const [font,setFont] = useState();
useEffect( () =>{
setFont(loadFont());
},[]);
return (
<FontFamilyContext.Provider value={font} >
<Screen />
<ScreenViaProp fontFamily={font} />
</FontFamilyContext.Provider>
);
}
export default App;
//Screen.jsx
//Advantage The font family can be used in nested components deep down
import { useContext } from "react";
import {FontContext} from "./App";
const Screen = () =>{
const fontFamily = useContext(FontFamilyContext);
return (
<div style={{fontFamily: fontFamily}}>
</div>
)
}
// ScreenViaProp .jsx Easier and no context is required
const ScreenViaProp = ({fontFamily}) =>{
return (
<div style={{fontFamily: fontFamily}}>
</div>
)
}

const DEFAULT_FONT_FAMILY = 'Helvetica-Bold';
const AppText = ({children, style, ...rest}) => {
const [fontFamily, setFontFamily] = useState(DEFAULT_FONT_FAMILY);
useEffect(() => {
// mock API
setTimeout(() => {
console.log('AppText: get font family name from API!!');
const FETCHED_FONT_FAMILY = 'HelveticaNeue';
if (FETCHED_FONT_FAMILY !== fontFamily) setFontFamily(FETCHED_FONT_FAMILY);
}, 200);
}, []);
return (
<Text
style={[
styles.text,
style,
{
fontFamily: fontFamily,
},
]}
{...rest}>
{children}
</Text>
);
};

You can also use a custom hook to load the font value only once in you app :
customHooks.js
let fontFromApi = null;
const fontP = new Promise(resolve => {
setTimeout(() => {
console.log('get font family name from API!!');
fontFromApi = 'HelveticaNeue';
resolve(fontFromApi);
}, 200);
});
export function useFont() {
const [font, setFont] = useState(fontFromApi || "Helvetica-Bold");
if (fontFromApi === null) {
fontP.then(v => setFont(v));
}
return font;
}
I declared a custom hook called useFont returning the font from the API. If the font is not loaded yet, it will return the fallback value Helvetica-Bold.
AppText.jsx
const { useFont } from './customHooks'
const AppText = ({children, style, ...rest}) => {
const fontFamily = useFont();
return (
<Text
style={[
styles.text,
style,
{ fontFamily }
]}
{...rest}>
{children}
</Text>
);
};

Related

Content provider data no being loaded into a component

I have the main page 'feed' where i used to have three functions, but I moved it into custom context. The console logs in context file output all the objects correctly, but nothing is visible in feed when i concole.log them.
context file:
import React, { useEffect, createContext, useContext, useState } from 'react'
import { useMemo } from 'react';
import getPosts from '../api/getPosts';
import filterImportedPosts from '../utils/filterImportedPosts';
export const ItemContext = createContext({
postData: {}, setPostData: () => { }
});
export const FilteredItemsContext = createContext({ filteredItems: [], setFilteredItems: () => { } })
export const FilterContext = createContext({ filter: '', setFilter: () => { } })
export function useItemContext() {
return useContext(ItemContext)
}
export function useFilteredItemsContext() {
return useContext(FilteredItemsContext)
}
export function useFilterContext() {
return useContext(FilterContext)
}
export default function PostProvider({ children }) {
const [postData, setPostData] = useState([]);
const [filteredItems, setFilteredItems] = useState([]);
const [filter, setFilter] = useState('');
useEffect(() => {
getPosts(setPostData)
console.log('postData: ', postData)
}, []);
useEffect(() => {
// console.log(filter);
const tempFiltItems = filterImportedPosts(postData, filter);
setFilteredItems(tempFiltItems);
console.log('tempFiltItems: ', filteredItems)
}, [filter, postData]);
const filteredItemsState = useMemo(() => {
return { filteredItems, setFilteredItems }
}, [filteredItems, setFilteredItems])
return (
<FilterContext.Provider value={{ filter, setFilter }}>
<FilteredItemsContext.Provider value={filteredItemsState}>
<ItemContext.Provider value={{ postData, setPostData }}>
{children}
</ItemContext.Provider>
</FilteredItemsContext.Provider >
</FilterContext.Provider>
)
}
and here the feed file:
import React, { useState, useEffect } from 'react';
import SinglePost from '../components/singlePost/singlePost';
import FilterPane from '../components/filterPane/filterPane.feedPost';
import { Box, Spinner, Text } from '#chakra-ui/react';
import getPosts from '../api/getPosts';
import Loader from '../../common/Loader';
import filterImportedPosts from '../utils/filterImportedPosts';
import PostProvider, { useFilterContext, useFilteredItemsContext, useItemContext } from './../context/PostDataContext';
export default function Feed() {
//-----------------IMPORT DATA FROM SERVER----------------------
const [error, setError] = useState(null);
const { postData, setPostData } = useItemContext();
const { filter, setFilter } = useFilterContext();
const { filteredItems, setFilteredItems } = useFilteredItemsContext();
useEffect(() => {
console.log(filteredItems)
}, [filteredItems, setFilteredItems])
// this helps while the data is loaded
// if (postData.length === 0) {
// return (
// <Box pos='absolute' top='45vh' left='40%'>
// <Loader />
// </Box>
// )
// }
// console.log(filteredItems);
return (
<PostProvider>
<Box mt={'7vh'} mb={'7vh'} ml={'3vw'} mr={'3vw'} zIndex={200}>
<FilterPane
setFilter={setFilter}
filter={filter}
filteredItems={filteredItems}
/>
{error && (
<div>Error occurred while loading profile info. Details: {error}</div>
)}
{!error && (
<>
{filteredItems.map((item, index) => {
return <SinglePost key={index} item={item} />;
})}
</>
)}
</Box>
</PostProvider>
);
}
console window printscreen. As you can see the filteredItems in context exist but nothing gets shown in the actual feed - the objects are empty. Could someone assist please?

how can i use default props in react.js or react-native?

For example. i have Feeds and Upload Components. also i have ImageFeedList component in Feeds, Upload Components
(Feed.js)
import React, {useContext, useState, useEffect} from 'react';
import {StackNavigationProp} from '#react-navigation/stack';
import {RandomUserDataContext} from '~/Context/RandomUserData';
import ImageFeedList from '~/Components/ImageFeedList';
type NavigationProp = StackNavigationProp<FeedsTabParamList, 'Feeds'>;
interface Props {
navigation: NavigationProp;
}
const Feeds = ({navigation}: Props) => {
const {getMyFeed} = useContext(RandomUserDataContext);
const [feedList, setFeedList] = useState<Array<IFeed>>([]);
const [loading, setLoading] = useState<boolean>(false);
useEffect(() => {
setFeedList(getMyFeed(24));
}, []);
return (
<ImageFeedList
feedList={feedList}
loading={loading}
onRefresh={() => {
setLoading(true);
setTimeout(() => {
setFeedList(getMyFeed(24));
setLoading(false);
}, 2000);
}}
onEndReached={() => {
setFeedList([...feedList, ...getMyFeed(24)]);
}}
onPress={() => {
navigation.navigate('FeedListOnly');
}}
/>
);
};
export default Feeds;
(Upload.js)
import React, {useContext, useState, useEffect} from 'react';
import {RandomUserDataContext} from '~/Context/RandomUserData';
import ImageFeedList from '~/Components/ImageFeedList';
const Upload = () => {
const {getMyFeed} = useContext(RandomUserDataContext);
const [feedList, setFeedList] = useState<Array<IFeed>>([]);
const [loading, setLoading] = useState<boolean>(false);
useEffect(() => {
setFeedList(getMyFeed(24));
}, []);
return (
<ImageFeedList
feedList={feedList}
loading={loading}
onRefresh={() => {
setLoading(true);
setTimeout(() => {
setFeedList(getMyFeed(24));
setLoading(false);
}, 2000);
}}
onEndReached={() => {
setFeedList([...feedList, ...getMyFeed(24)]);
}}
/>
);
};
export default Upload;
(ImageFeedList.js)
import React from 'react';
import {
FlatList,
Image,
Dimensions,
NativeSyntheticEvent,
NativeScrollEvent,
} from 'react-native';
import styled from 'styled-components/native';
interface Props {
id?: number;
bounces?: boolean;
scrollEnabled?: boolean;
feedList: Array<IFeed>;
loading?: boolean;
onRefresh?: () => void;
onEndReached?: () => void;
onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
onPress?: () => void;
}
const ImageFeedList = ({
id,
bounces = true,
scrollEnabled = true,
feedList,
loading,
onRefresh,
onEndReached,
onScroll,
onPress,
}: Props) => {
const width = Dimensions.get('window').width;
const imageWidth = width / 3;
return (
<FlatList
data={feedList}
style={{width}}
keyExtractor={(item, index) => {
return `image-feed-${id}-${index}`;
}}
showsVerticalScrollIndicator={false}
scrollEnabled={scrollEnabled}
bounces={bounces}
numColumns={3}
onRefresh={onRefresh}
onEndReached={onEndReached}
onEndReachedThreshold={0.5}
refreshing={loading}
onScroll={onScroll}
scrollEventThrottle={400}
renderItem={({item, index}) => (
<ImageContainer
style={{
paddingLeft: index % 3 === 0 ? 0 : 1,
paddingRight: index % 3 === 2 ? 0 : 1,
}}
onPress={onPress}>
<Image
source={{uri: item.images[0]}}
style={{width: imageWidth, height: imageWidth}}
/>
</ImageContainer>
)}
/>
);
};
export default ImageFeedList;
what i want to ask is that in Feeds Component i have OnPress and i can pass onPress to ImageFeedList component as Props but there is no onPress props in Upload Component. however error is not gonna happen eventhough there is no onPress in Upload because i have a
interface Props {
onPress?: () => void;
}
this code i define onPress Props in In ImageFeedList components it meanse if i don't get Props onPress then it's fine
i can use this default props in typeScript
but my question is that how can i use default props in react and react-native other than typeScript??
is there way??
You are already using default props in ImageFeedList.
const ImageFeedList = ({
id,
bounces = true, /* <<< has a default value "true"*/
scrollEnabled = true, /* <<< has a default value "true"*/
...
onPress /* <<< has no defaut value*/,
}: Props) => {...}
you could add something like that as a default for onPress
const ImageFeedList = ({
...
onPress = () => console.log(), /* a default prop as an arrow function */
}: Props) => {...
Here is another example of how to pass a function as a default parameter
function y(prop = function () {console.log("I log default")}){
prop();
};
y(); // will log "I log default"
y(function () {console.log("I am not a default");}); // will log "I am not a default"
Update:
You could also check if onPress is defined, before using it.
<ImageContainer
...
onPress={() => typeof onPress === "function" && onPress()}>

reactjs createRef not work in component arrays

like this i hava a array of components need ref to trigger the comment component collapse, so i need to create some refs to reference each commentListItem, but it doesn't work, how do i do this work?
import React, { useRef, createRef } from "react";
import PropTypes from "prop-types";
import { map, isArray } from "lodash/fp";
import Divider from "#material-ui/core/Divider";
import CommentListItem from "./CommentListItem";
import CommentCollapse from "./CommentCollapse";
function CommentList({ list = [], ...props }) {
const { count = 0 } = props;
const refList = map((o) => {
/* o.ref = createRef(null); */
return o;
})(list);
const onShow = () => {
console.log(refList);
};
return (
<div className="ke-comment-list">
{map.convert({ cap: false })((o, i) => (
<div key={i} className="ke-comment-list-item">
<CommentListItem listItem={o} onShow={onShow} />
{isArray(o.child) && o.child.length ? (
<CommentCollapse {...o}>
<CommentList list={o.child} count={count + 1} />
</CommentCollapse>
) : null}
{count > 0 && list.length - 1 === i ? null : <Divider />}
</div>
))(refList)}
</div>
);
}
CommentList.propTypes = {
list: PropTypes.arrayOf(PropTypes.object).isRequired,
};
export default CommentList;
there is CommentCollapse component for show or hide subcomment.
import React, { useState, forwardRef, useImperativeHandle } from "react";
import ButtonBase from "#material-ui/core/ButtonBase";
import Collapse from "#material-ui/core/Collapse";
const CommentCollapse = ({ children }, ref) => {
const [show, setShow] = useState(false);
const showMore = () => {
setShow((prev) => !prev);
};
const collapseText = () => (show ? "收起" : "展开");
useImperativeHandle(ref, () => ({
showMore: showMore()
}));
return (
<div className="ke-comment-list-children">
<Collapse in={show}>{children}</Collapse>
<ButtonBase size="small" onClick={showMore}>
{collapseText()}
</ButtonBase>
</div>
);
};
export default forwardRef(CommentCollapse);
catch errors
Uncaught Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
have any idear for this situation?
is fixed, just not trigger showMore function in ref.
import React, { useState, forwardRef, useImperativeHandle } from "react";
import ButtonBase from "#material-ui/core/ButtonBase";
import Collapse from "#material-ui/core/Collapse";
const CommentCollapse = ({ children }, ref) => {
const [show, setShow] = useState(false);
const showMore = () => {
setShow((prev) => !prev);
};
const collapseText = () => (show ? "收起" : "展开");
useImperativeHandle(ref, () => ({
showMore
}));
return (
<div className="ke-comment-list-children">
<Collapse in={show}>{children}</Collapse>
<ButtonBase size="small" onClick={showMore}>
{collapseText()}
</ButtonBase>
</div>
);
};
export default forwardRef(CommentCollapse);

How do I return firestore collection as json object?

I have a family tree component that works when I import the data from a json file.
I would like to use the data within my firestore collection 'family'
Below is my parent component where I am calling the data
import React from 'react';
import { Grid } from 'semantic-ui-react';
import FamilyTree from '../FamilyTree/FamilyTree';
import { useSelector, useDispatch } from 'react-redux';
import { listenToFamilyFromFirestore } from '../../../app/firestore/firestoreService';
import { listenToFamily } from '../familyActions';
import useFirestoreCollection from '../../../app/hooks/useFirestoreCollection';
export default function FamilyDashboard() {
const dispatch = useDispatch();
const { family } = useSelector((state) => state.family);
useFirestoreCollection({
query: () => listenToFamilyFromFirestore(),
data: (family) => dispatch(listenToFamily(family)),
deps: [dispatch],
});
return (
<Grid>
<Grid.Column width={16}>
<FamilyTree family={family} />
</Grid.Column>
</Grid>
);
}
Here is my child component
import React, { Fragment, useState } from 'react';
import PinchZoomPan from 'pinch-zoom-pan';
import { IFamilyNode, IFamilyExtNode } from 'relatives-tree';
import ReactFamilyTree from 'react-family-tree';
import FamilyNode from '../../family/FamilyNode/FamilyNode';
import styles from '../FamilyTree/Family.module.css';
import nodes from '../../../app/api/family.json';
import { objectToArray } from '../../../app/common/util/util';
const myID = 'palmasandora';
const WIDTH = 70;
const HEIGHT = 110;
export default React.memo<{}>(function FamilyTree(family) {
const [menuActive] = useState(false);
const [rootId, setRootId] = useState<string>(myID);
const modes = JSON.parse(JSON.stringify(family));
//const onResetClick = useCallback(() => setRootId(myID), []);
console.log('family', family);
console.log('modes', modes);
// console.log('test', JSON.parse(JSON.stringify(family)));
console.log('json nodes', nodes);
return (
<Fragment>
<div className={styles.root}>
<PinchZoomPan
//debug
captureWheel
min={0.3}
max={2.5}
className={`myCanvas ${styles.wrapper} ${
menuActive ? `${styles.ADDED_CLASS}` : ''
}`}
>
<ReactFamilyTree
nodes={nodes as IFamilyNode[]}
rootId={rootId}
width={WIDTH}
height={HEIGHT}
canvasClassName={styles.tree}
renderNode={(node: IFamilyExtNode) => (
<FamilyNode
key={node.id}
node={node}
isRoot={node.id === rootId}
onSubClick={setRootId}
style={{
top: '10px',
width: WIDTH,
height: HEIGHT,
transform: `translate(${node.left * (WIDTH / 2)}px, ${
node.top * (HEIGHT / 2)
}px)`,
}}
/>
)}
/>
</PinchZoomPan>
{/* {rootId !== myID && (
<div className={styles.reset} onClick={onResetClick}>Reset</div>
)} */}
</div>
</Fragment>
);
});
Below is a screen shot of my console log:
I would like to use the information within the first family array. It should look more like the json object being returned below.
I was able to store this correctly in my state, maybe I should use that? How would I go about that?
As you using Flatten data Structures, you can codes similar to codes below.
You can use the once() method to simplify this scenario: it triggers once and then does not trigger again.
var userId = firebase.auth().currentUser.uid;
return firebase.database().ref('/users/' + userId).once('value').then(function(snapshot) {
var username = (snapshot.val() && snapshot.val().username) || 'Anonymous';
// ...
});

Implementing infinite scroll in React with Apollo Client

In my NextJS app, I have a PostList.jsx component that looks like this:
import { useQuery } from '#apollo/react-hooks';
import Typography from '#material-ui/core/Typography';
import { NetworkStatus } from 'apollo-client';
import gql from 'graphql-tag';
import getPostsQuery from '../../apollo/schemas/getPostsQuery.graphql';
import Loading from './Loading';
import Grid from '#material-ui/core/Grid';
import PostPreview from './PostPreview';
import withStyles from '#material-ui/core/styles/withStyles';
import React, { useLayoutEffect } from 'react';
const styles = (theme) => ({
root: {
padding: theme.spacing(6, 2),
width: '100%',
},
});
export const GET_POSTS = gql`${getPostsQuery}`;
export const getPostsQueryVars = {
start: 0,
limit: 7,
};
const PostsList = (props) => {
const { classes } = props;
const {
loading,
error,
data,
fetchMore,
networkStatus,
} = useQuery(
GET_POSTS,
{
variables: getPostsQueryVars,
// Setting this value to true will make the component rerender when
// the "networkStatus" changes, so we'd know if it is fetching
// more data
notifyOnNetworkStatusChange: true,
},
);
const loadingMorePosts = networkStatus === NetworkStatus.fetchMore;
const loadMorePosts = () => {
fetchMore({
variables: {
skip: posts.length
},
updateQuery: (previousResult, { fetchMoreResult }) => {
if (!fetchMoreResult) {
return previousResult
}
return Object.assign({}, previousResult, {
// Append the new posts results to the old one
posts: [...previousResult.posts, ...fetchMoreResult.posts]
})
}
})
};
const scrollFunction = () => {
const postsContainer = document.getElementById('posts-container');
if (postsContainer.getBoundingClientRect().bottom <= window.innerHeight) {
console.log('container bottom reached');
}
};
useLayoutEffect(() => {
document.addEventListener('scroll', scrollFunction);
scrollFunction();
// returned function will be called on component unmount
return () => {
document.removeEventListener('scroll', scrollFunction);
};
}, []);
if (error) return <div>There was an error!</div>;
if (loading) return <Loading />;
const { posts, postsConnection } = data;
const areMorePosts = posts.length < postsConnection.aggregate.count;
return (
<Grid item className={classes.root}>
<Grid container spacing={2} direction="row" id="posts-container">
{posts.map((post) => {
return (
<Grid item xs={12} sm={6} md={4} lg={3} xl={2} className={`post-preview-container`}>
<PostPreview
title={post.title}
excerpt={post.excerpt}
thumbnail={`https://i.schandillia.com/d/${post.thumbnail.hash}${post.thumbnail.ext}`}
/>
</Grid>
);
})}
</Grid>
{areMorePosts && (
<button onClick={() => loadMorePosts()} disabled={loadingMorePosts}>
{loadingMorePosts ? 'Loading...' : 'Show More'}
</button>
)}
</Grid>
);
};
export default withStyles(styles)(PostsList);
As you can see, this component fetches documents from a database via a GraphQL query using Apollo Client and displays them paginated. The pagination is defined by the getPostsQueryVars object. Here, if you scroll down to the bottom and there still are posts available, you'll get a button clicking which the next set of posts will be loaded.
What I'm keen on doing here is implement some kind of an infinite scroll and do away with the button altogether. So far, I've added a scroll event function to the component using React hooks and can confirm it's triggering as expected:
const scrollFunction = () => {
const postsContainer = document.getElementById('posts-container');
if (postsContainer.getBoundingClientRect().bottom <= window.innerHeight) {
console.log('container bottom reached');
}
};
useLayoutEffect(() => {
document.addEventListener('scroll', scrollFunction);
scrollFunction();
return () => {
document.removeEventListener('scroll', scrollFunction);
};
}, []);
But how do I proceed from here? How do achieve the following once the container bottom is reached AND areMorePosts is true:
Display a <h4>Loading...</h4> right before the last </Grid>?
Trigger the loadMorePosts() function?
remove <h4>Loading...</h4> once loadMorePosts() has finished executing?

Categories