State change does not effect React DOM - reactjs

I am using React Material UI. For loading purposes, I decided to use Skeleton structure. However, state change does not effect skeleton component's width and height so that it does not work properly.
export default function ComplexGrid(props) {
const [hover, setHover] = useState(false);
const [article, setArticle] = useState("");
const [year, setYear] = useState("");
const [month, setMonth] = useState("");
const [loaded, setLoaded] = useState(false);
const [width, setWidth] = useState(null);
const [height, setHeight] = useState(null);
const skeletonStructure = useRef(null);
const classes = useStyles();
useEffect(() => {
if (props.mainNew[0]) {
setArticle(props.mainNew[0]);
setYear(String(article.updated_at).split("-")[0]);
setMonth(String(article.updated_at).split("-")[1]);
}
if (skeletonStructure.current) {
setWidth(skeletonStructure.current.offsetWidth);
setHeight(skeletonStructure.current.offsetHeight);
}
setTimeout(() => {
setLoaded(true);
}, 500);
});
const stylehvr = loaded && hover && article ? { cursor: "pointer", opacity: "60%", transition: "all 0.5s linear" } : {};
const hoverbck = loaded && hover && article ? { backgroundColor: "black", cursor: "pointer" } : {};
return (
<div className={classes.root} onMouseOver={() => setHover(true)} onMouseOut={() => setHover(false)}>
<Grid container spacing={2} className={classes.grd}>
<div style={hoverbck}>
{article && loaded ? (
<img ref={skeletonStructure} className={classes.img} style={stylehvr} alt={article.title} src={article.img1} />
) : (
<Skeleton variant="rect" width={width} height={height} animation="pulse" />
)}
{article && loaded ? (
<Typography gutterBottom variant="subtitle1" className={classes.title}>
{article.title} - {width} - {height}
</Typography>
) : (
""
)}
{article && loaded ? (
<Typography variant="body2" color="textSecondary" className={classes.date}>
{month} / {year}
</Typography>
) : (
""
)}
{article && loaded ? (
<Typography variant="body2" color="textSecondary" className={classes.timer}>
{article.read_time} Dakikalık Okuma
</Typography>
) : (
""
)}
{article && loaded ? (
<Typography variant="body2" className={classes.type}>
{article.content_type}
</Typography>
) : (
""
)}
</div>
</Grid>
</div>
);
}
Since initial state of height and weight are 'null', skeleton does not work as intended, nevertheless weight and height are correctly set in useEffect. How to use correct state for skeleton component?

Try setting all your states as dependency in the useEffect hooks
useEffect(() => {//your code}, [//all states here])

To give dependency to UseEffect You need to give it like this.
useEffect(() => {
//your code which should be executed when dependent state updates.
}, [state names])
//UPDATED
useEffect(() => {
if(props.mainNew[0]){
setArticle(props.mainNew[0])
setYear(String(article.updated_at).split("-")[0])
setMonth(String(article.updated_at).split("-")[1])
}
if(skeletonStructure.current){
setWidth(skeletonStructure.current.offsetWidth)
setHeight(skeletonStructure.current.offsetHeight)
}
setTimeout(() => {
setLoaded(true)
}, 500)
},[skeletonStructure.current,props.mainNew[0]])
if you don't provide dependent states within [] then it will not be rendered again when state update occurs. and if you provide [] empty then use-effect will be called once only.

Related

How can I scan through the list of string array items to identify which part matches regex expression for url and convert it to link w react typescri

IMessage is an interface with type string. The app is something like a todo list, but I need it to identify when a URL is typed in and convert it to a clickable link
const [message, setMessage] = useState<string>("");
const [chat, setChat] = useState<IMessage[]>([]);
const regex = /https?:\/\/(www\.)?[-a-zA-Z0-9#:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()#:%_\+.~#?&//=]*)/;
useEffect(() => {
chat.forEach(chat => {
///function
})
})
here's a bigger piece of code that I have at the moment
const Home: NextPage = () => {
const [message, setMessage] = useState<string>("");
const [chat, setChat] = useState<IMessage[]>([]);
const regex = /https?:\/\/(www\.)?[-a-zA-Z0-9#:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()#:%_\+.~#?&//=]*)/;
const sendMessage = () => {
const newMessage = { message: message };
setChat([...chat, newMessage]);
setMessage("");
};
const inputChanged = (
event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => setMessage(event.target.value);
return (
<div className={styles.container}>
<Stack
direction="column"
spacing={2}
justifyContent="center"
alignItems="flex-end"
>
{chat.map((message: IMessage) => {
return (
<Paper
style={{
backgroundColor: "#6096ba",
padding: "5px",
borderRadius: "30px",
}}
elevation={3}
>
<p style={{ maxWidth: "20ch", color: "white" }}>
{message.message}
</p>
</Paper>
);
})}
</Stack>
```
Here you can use it.includes() if I got you right, Then you add whatever you want to do with it.
-Another tip: Create your function out of useEffect then just call it inside the effect, you might have a big project then you will find it harder and not useful at all
You don't need to use useEffect for this if you want it to change the rendering to be a link.
Your return would look something like the following. You would choose to either render as plain text or as a link.
return (
<ol>
{chat.map((chatMessage) => {
return chatMessage.match(regex) ? (
<li>
<a href={chatMessage}>{chatMessage}</a>
</li>
) : (
<li>{chatMessage}</li>
);
})}
</ol>
);

ReactJS: Set priority order of component - if field exists

I have the following:
const useItemsF = () => {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const user = firebase.auth().currentUser;
useEffect(() => {
const unsubscribe = firebase
.firestore()
.collection("user")
.where("id", "==", `${user.uid}`)
.onSnapshot(snapshot => {
const listItemsUsers = snapshot.docs.map(doc => ({
id: doc.id,
...doc.data()
}));
setItems(listItemsUsers);
setLoading(false);
});
console.log(loading);
return () => unsubscribe();
}, []);
return items;
};
I then use the following in my return:
const ItemList = () => {
const listItemF = useItemsF();
}
return (
<>
<div className="wrapper">
<h1><FontAwesomeIcon icon={faTachometer} size="lg" /></h1>
<Card elevation={3} className={classes.card}>
<CardContent className="cardoverride">
{listItemF?.length && listItemF[0]?.userProfilePic?.userPicURL !== 'undefined' ? <img className="dashimage" src={listItemF[0]?.userProfilePic?.userPicURL} /> : (
<img className="dashimage" src="/Image.jpg" />
)}
</CardContent>
</Card>
</div>
</>
);
export default App;
This works fine, but what I end up seeing in Image.jpg before the userProfilePic. How can I restructure this so you don't see the Image.jpg at all? So if the user has set a Profile Pic, this shows instantly (or at least does not show the Image.jpg)
so what is happening is, the component is rendering first (at this time, items.userProfilePik doesnt exist, so the /Image.jpg is rendered). Once the comp is done rendering, then your useEffect fires, where your firebase stuff is happening, which i'm guessing fetches the profilePik from the database and sets state using setItems. Only now, will your items.userProfilePik render.
One way to solve this problem is initiate another state as such:
const [loading, setLoading] = useState(true)
//then in your useEffect
//...
setItems(listItemsUsers);
setLoading(false); //add this line
//...
//then in your return statement do something like:
return (
<>
<div className="wrapper">
<h1><FontAwesomeIcon icon={faTachometer} size="lg" /></h1>
<Card elevation={3} className={classes.card}>
<CardContent className="cardoverride">
{ loading
? <div>Loading...</div>
: listItemF?.length && listItemF[0]?.userProfilePic?.userPicURL !== undefined
? <img className="dashimage" src={listItemF[0]?.userProfilePic?.userPicURL} />
: <img className="dashimage" src="/Image.jpg" /> }
</CardContent>
</Card>
</div>
</>
);

Update Child state from Parent using Context in React

I have a few buttons and "view all" button. The individual buttons load the coresponding data of that index or will show all the data by clicking the "view all" button. Problem I am running into is when I click my "view all" button in the parent it's not updating the state in the child component. On mounting it works as normal but on event handler in the "view all" it doesn't update. Any thoughts on where I am going wrong here?
JS:
...
const Context = createContext(false);
const useStyles = makeStyles((theme) => ({
root: {
display: "flex",
"& > *": {
margin: theme.spacing(1)
}
},
orange: {
color: theme.palette.getContrastText(deepOrange[500]),
backgroundColor: deepOrange[500],
border: "4px solid black"
},
info: {
margin: "10px"
},
wrapper: {
display: "flex"
},
contentWrapper: {
display: "flex",
flexDirection: "column"
},
elWrapper: {
opacity: 0,
"&.active": {
opacity: 1
}
}
}));
const ToggleItem = ({ id, styles, discription }) => {
const { activeViewAll, handleChange } = useContext(Context);
const [toggleThisButton, setToggleThisButton] = useState();
const handleClick = () => {
setToggleThisButton((prev) => !prev);
handleChange(discription, !toggleThisButton);
};
return (
<>
<Avatar
className={toggleThisButton && !activeViewAll ? styles.orange : ""}
onClick={handleClick}
>
{id}
</Avatar>
<p>{JSON.stringify(toggleThisButton)}</p>
</>
);
};
const ToggleContainer = ({ className, selected }) => {
return (
<div className={className}>
{selected.map((item, idx) => (
<div key={idx}>Content {item}</div>
))}
</div>
);
};
export default function App() {
const data = ["first", "second", "third"];
const classes = useStyles();
const [selected, setSelected] = useState([]);
const [activeViewAll, setActiveViewAll] = useState(false);
useEffect(() => {
setActiveViewAll(true);
setSelected([...data]);
}, []);
const handleChange = (val, action) => {
let newVal = [];
if (activeViewAll) {
selected.splice(0, 3);
setActiveViewAll(false);
}
if (action) {
newVal = [...selected, val];
} else {
// If toggle off, then remove content from selected state
newVal = selected.filter((v) => v !== val);
}
console.log("action", action);
setSelected(newVal);
};
const handleViewAll = () => {
console.log("all clicked");
setActiveViewAll(true);
setSelected([...data]);
};
return (
<Context.Provider value={{ activeViewAll, handleChange }}>
<div className={classes.wrapper}>
<Avatar
className={activeViewAll ? classes.orange : null}
onClick={handleViewAll}
>
<span style={{ fontSize: "0.75rem", textAlign: "center" }}>
View All
</span>
</Avatar>
{data.map((d, id) => {
return (
<div key={id}>
<ToggleItem id={id} styles={classes} discription={d} />
</div>
);
})}
</div>
<div className={classes.contentWrapper}>
<ToggleContainer styles={classes} selected={selected} />
</div>
</Context.Provider>
);
}
Codesanbox:
https://codesandbox.io/s/72166087-forked-jvn59i?file=/src/App.js:260-3117
Issue
The issue seems to be that you are mixing up the management of the boolean activeViewAll state with the selected state.
Solution
When activeViewAll is true, pass the data array as the selected prop value to the ToggleContainer component, otherwise pass what is actually selected, the selected state.
Simplify the handlers. The handleViewAll callback only toggles the view all state to true, and the handleChange callback toggles the view all state back to false and selects/deselects the data item.
function App() {
const data = ["first", "second", "third"];
const classes = useStyles();
const [selected, setSelected] = useState([]); // none selected b/c view all true
const [activeViewAll, setActiveViewAll] = useState(true); // initially view all
const handleChange = (val, action) => {
setActiveViewAll(false); // deselect view all
setSelected(selected => {
if (action) {
return [...selected, val];
} else {
return selected.filter(v => v !== val)
}
});
};
const handleViewAll = () => {
setActiveViewAll(true); // select view all
};
return (
<Context.Provider value={{ activeViewAll, handleChange }}>
<div className={classes.wrapper}>
<Avatar
className={activeViewAll ? classes.orange : null}
onClick={handleViewAll}
>
<span style={{ fontSize: "0.75rem", textAlign: "center" }}>
View All
</span>
</Avatar>
{data.map((d, id) => {
return (
<div key={id}>
<ToggleItem id={id} styles={classes} discription={d} />
</div>
);
})}
</div>
<div className={classes.contentWrapper}>
<ToggleContainer
styles={classes}
selected={activeViewAll ? data : selected} // pass all data, or selected only
/>
</div>
</Context.Provider>
);
}
In the ToggleContainer don't use the array index as the React key since you are mutating the array. Use the element value since they are unique and changing the order/index doesn't affect the value.
const ToggleContainer = ({ className, selected }) => {
return (
<div className={className}>
{selected.map((item) => (
<div key={item}>Content {item}</div>
))}
</div>
);
};
Update
Since it is now understood that you want to not remember what was previously selected before toggling activeViewAll then when toggling true clear the selected state array. Instead of duplicating the selected state in the children components, pass the selected array in the context and computed a derived isSelected state. This maintains a single source of truth for what is selected and removes the need to "synchronize" state between components.
const ToggleItem = ({ id, styles, description }) => {
const { handleChange, selected } = useContext(Context);
const isSelected = selected.includes(description);
const handleClick = () => {
handleChange(description);
};
return (
<>
<Avatar
className={isSelected ? styles.orange : ""}
onClick={handleClick}
>
{id}
</Avatar>
<p>{JSON.stringify(isSelected)}</p>
</>
);
};
const ToggleContainer = ({ className, selected }) => {
return (
<div className={className}>
{selected.map((item) => (
<div key={item}>Content {item}</div>
))}
</div>
);
};
Update the handleChange component to take only the selected value and determine if it needs to add/remove the value.
export default function App() {
const data = ["first", "second", "third"];
const classes = useStyles();
const [selected, setSelected] = useState([]);
const [activeViewAll, setActiveViewAll] = useState(true);
const handleChange = (val) => {
setActiveViewAll(false);
setSelected((selected) => {
if (selected.includes(val)) {
return selected.filter((v) => v !== val);
} else {
return [...selected, val];
}
});
};
const handleViewAll = () => {
setActiveViewAll(true);
setSelected([]);
};
return (
<Context.Provider value={{ activeViewAll, handleChange, selected }}>
<div className={classes.wrapper}>
<Avatar
className={activeViewAll ? classes.orange : null}
onClick={handleViewAll}
>
<span style={{ fontSize: "0.75rem", textAlign: "center" }}>
View All
</span>
</Avatar>
{data.map((d, id) => {
return (
<div key={d}>
<ToggleItem id={id} styles={classes} description={d} />
</div>
);
})}
</div>
<div className={classes.contentWrapper}>
<ToggleContainer
styles={classes}
selected={activeViewAll ? data : selected}
/>
</div>
</Context.Provider>
);
}

Create custom Search bar in react to search through Firebase document

I want to create a custom search bar to query my Firestore document retrieve collection based on user input.
I know there are better options to do this like Algolia, Typesense etc.
But I have issues with Firebase upgrading my account, and I have contacted the Firebase team.
DrinkSearch.tsx
const DrinkSearch: React.FC = () => {
const [searchTerm, setSearchTerm] = useState("");
const [drinkSnap, setDrinkSnap] = useState<
QueryDocumentSnapshot<DocumentData>[]
>([]);
const [isLoading, setIsLoading] = useState(false);
const drinkRef = collection(firebaseFirestore, "products");
const drinkQuery = query(drinkRef, where("drinkName", "==", searchTerm));
const snapshots = getDocs(drinkQuery);
let docsIsEmpty!: boolean;
const getProductOnChange = () => {
setIsLoading(true);
snapshots
.then((docsSnapshot) => {
setIsLoading(false);
setDrinkSnap(docsSnapshot?.docs);
docsIsEmpty = docsSnapshot?.empty;
console.log(docsSnapshot?.docs);
})
.catch((error: FirestoreError) => {
setIsLoading(false);
console.log(error.message);
});
};
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setSearchTerm(e.currentTarget.value);
getProductOnChange();
};
useEffect(() => {
console.log(searchTerm);
}, [searchTerm]);
return (
<Box>
<InputGroup size="lg">
<InputLeftElement pointerEvents="none">
<RiSearch2Line color="#CBD5E0" size="20px" />
</InputLeftElement>
<Input
onChange={handleChange}
type="text"
_focus={{
boxShadow: shadowSm,
}}
fontSize="14px"
placeholder="Search for drinks"
/>
</InputGroup>
<Box
padding={5}
bgColor="white"
height="40px"
borderBottomRadius={"8px"}
border={"1px solid #EDF2F7"}
>
{docsIsEmpty && <Text>Drink not found.</Text>}
{isLoading && (
<Flex height="100%">
<Spinner size={"sm"} colorScheme={"primary.500"} />
</Flex>
)}
{drinkSnap &&
drinkSnap?.map((drinkSnap) => {
const drinks = drinkSnap?.data();
return (
<HStack
cursor={"pointer"}
justify={"space-between"}
padding={"5px"}
_hover={{
bgColor: "#EDF2F7",
}}
key={drinkSnap?.id}
>
<Text fontWeight={"semibold"}>{drinks?.drinkName}</Text>
<Badge fontSize={"12px"}>{drinks?.category}</Badge>
</HStack>
);
})}
</Box>
</Box>
);
};
export default DrinkSearch;
Result: When I start typing for example black label is the name of a drink, nothing happens i.e the [] is empty. When I remove 'l'. it remains black labe, it returns the array with the collection.
What I want: On typing, return all collections that match what is typed.

Trying to conditional render css to on alternating items in an array with useState in React with MUI

I have an object of arrays that I'm mapping over called featuredProj, and on alternate items, I want the CSS to conditionally render as not to repeat so much code. Using useState in the handleSide function I get the error too many rerenders. How can I solve this, or is there a better solution to rendering jsx while mapping over an array of objects.
const useStyles = makeStyles(() => ({
title: {
textAlign: (side) => (side ? "right" : "left"),
},
}));
const FeaturedProjects = () => {
const [side, setSide] = useState(true);
const classes = useStyles(side);
const handleSide = (project, index) => {
if (index === 0 || index % 2 === 0) {
// I tried setSide(false), setSide(prev => !prev)
return (
<Grid Container key={index}>
<Typography className={classes.title}>{project.title}</Typography>
</Grid>
);
} else {
return (
<Grid Container key={index}>
<Typography className={classes.title}>{project.title}</Typography>
</Grid>
);
}
};
return (
<Container>
{featuredProj.map((proj, ind) => (
<Reveal duration="2000" effect="fadeInUp">
{handleSide(proj, ind)}
</Reveal>
))}
</Container>
);
};
Thanks in advance for any assistance!
You cannot call setState() inside render method. setState() will trigger a re-rendering which calls render method again which leads to another setState().. you get the idea.
If you want it to work, you need to create separate component with the side props and pass it as an argument to your style hook.
const FeaturedProjects = () => {
const classes = useStyles(side);
return (
<Container>
{featuredProj.map((proj, ind) => (
<FeaturedProjectItem
key={index}
project={proj}
side={index === 0 || index % 2 === 0}
/>
))}
</Container>
);
};
const useStyles = makeStyles({
title: {
textAlign: (side) => (side ? "right" : "left"),
},
});
const FeaturedProjectItem = ({ side, project }) => {
const classes = useStyles(side);
return (
<Reveal duration="2000" effect="fadeInUp">
<Grid Container>
<Typography className={classes.title}>{project.title}</Typography>
</Grid>
</Reveal>
);
};

Resources