I have the following reducer that sets a width or a hight dimension
I'm thinking something like this
const dimensionsReducer = (state = {width: 300, height: 250}, action)
=> {
switch(action.type) {
case 'SET_WIDTH':
return {
...state,
width: action.value
}
case 'SET_HEIGHT':
return {
...state,
height: action.value
}
default:
return state;
}
}
export default dimensionsReducer;
I'd like to create a reducer that can set width and height at the same time from a list of presets
const presets = {
bigBox: {
width: 500,
height: 500
},
littleBox: {
width: 250,
height: 250
}
}
const presetReducer = (state = {preset: ""}, action) => {
switch (action.type) {
case 'SET_PRESET':
return {
...state,
preset: action.value
// how do I do this
// set width presets[action.value].width
// set height presets[action.value].height
}
default:
return state;
}
}
How can I reuse the dimensions reducer to set the widths and height?
Related
I have a dynamically sized collection of objects being passed into a Nav component that are being mapped and rendered as buttons. I want to apply a CSS animation to each button so that they slide in from off screen one at a time when the Nav component mounts. I figured that I would set up a loop through each one that updates a boolean value inside of a corresponding state object which applies the CSS class to the button to animate it, but each time that state object is updated, all of the buttons rerender which in turn starts all of the animations over. How can I prevent these rerenders?
// Nav.jsx
import React, { useState, useEffect } from 'react';
import { Button } from '../../../components';
import './Nav.scss';
const Nav = ({ actions }) => {
const [renderStates, setRenderStates] = useState(actions.reduce((accum, val) => {
return {...accum, [val.id]: false};
}, {}));
useEffect(() => {
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
const updateStates = async () => {
for (let i = 0; i < actions.length; i++) {
if (i > 0) {
await delay(75);
}
setRenderStates((prev) => ({
...prev,
[i]: true,
}));
};
};
updateStates();
}, [actions.length]);
return (
<div className='Nav'>
{actions.map((act) => (
<div className={`Nav__Button ${renderStates[act.id] ? 'Animate' : ''}`} key={act.id}>
<Button icon={act.icon} onClick={act.onClick} />
</div>
))}
</div>
);
};
export default Nav;
/* Nav.scss */
.Nav {
height: 100%;
width: fit-content;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
align-self: center;
padding: 1rem;
}
.Nav > * {
margin: 20% 0,
}
.Nav__Button {
margin-left: -5rem;
}
.Animate {
animation: slideInFromLeft .4s ease;
}
#keyframes slideInFromLeft {
0% {
margin-left: -5rem;
}
75% {
margin-left: .5rem;
}
100% {
margin-left: 0;
}
}
Here's a codesandbox that illustrates the problem (refresh the embedded browser to see the issue):
https://codesandbox.io/s/react-css-animations-on-timer-8mxnsz
Any help would be appreciated. Thanks.
You will need to create a from the elements inside actions.map and render a memoized version of it so that if the props do not change it will not re-render.
import { useState, useEffect, memo } from "react";
import "./styles.css";
const Test = ({ animate, label }) => {
return (
<div className={`Nav__Button ${animate ? "Animate" : ""}`}>
<button>{label}</button>
</div>
);
};
const TestMemo = memo(Test);
export default function App() {
const actions = [
{
id: 0,
label: "button 0"
},
{
id: 1,
label: "button 1"
},
{
id: 2,
label: "button 2"
},
{
id: 3,
label: "button 3"
}
];
const [renderStates, setRenderStates] = useState(
actions.reduce((accum, val) => {
return { ...accum, [val.id]: false };
}, {})
);
useEffect(() => {
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const updateStates = async () => {
for (let i = 0; i < actions.length; i++) {
if (i > 0) {
await delay(2000);
}
setRenderStates((prev) => ({
...prev,
[i]: true
}));
}
};
updateStates();
}, [actions.length]);
return (
<div className="App">
{actions.map((act) => (
<TestMemo animate={renderStates[act.id]} label={act.label} />
))}
</div>
);
}
I've almost got this working in a codesandbox, but the drop accuracy still isn't quite right. I'm at a loss for what else to try.
Here is my container:
import update from "immutability-helper";
import { useCallback, useState } from "react";
import { useDrop } from "react-dnd";
import { DraggableBox } from "./DraggableBox.js";
import { ItemTypes } from "./ItemTypes.js";
import { snapToGrid as doSnapToGrid } from "./snapToGrid.js";
const styles = {
width: 300,
height: 300,
border: "1px solid black",
position: "relative",
transform: "scale(.8)"
};
export const Container = ({ snapToGrid }) => {
const [boxes, setBoxes] = useState({
a: { top: 20, left: 80, title: "Drag me around" },
b: { top: 180, left: 20, title: "Drag me too" }
});
const moveBox = useCallback(
(id, left, top) => {
setBoxes(
update(boxes, {
[id]: {
$merge: { left, top }
}
})
);
},
[boxes]
);
const [, drop] = useDrop(
() => ({
accept: ItemTypes.BOX,
drop(item, monitor) {
const delta = monitor.getDifferenceFromInitialOffset();
let left = item.left + delta.x;
let top = item.top + delta.y;
if (snapToGrid) {
[left, top] = doSnapToGrid(left, top);
}
moveBox(item.id, left, top);
return undefined;
}
}),
[moveBox]
);
return (
<div ref={drop} style={styles}>
{Object.keys(boxes).map((key) => (
<DraggableBox key={key} id={key} {...boxes[key]} />
))}
</div>
);
};
Here is my draggable box:
import { memo, useEffect } from 'react'
import { useDrag } from 'react-dnd'
import { getEmptyImage } from 'react-dnd-html5-backend'
import { Box } from './Box.js'
import { ItemTypes } from './ItemTypes.js'
function getStyles(left, top, isDragging) {
const transform = `translate3d(${left }px, ${top }px, 0)`
return {
position: 'absolute',
transform,
WebkitTransform: transform,
// IE fallback: hide the real node using CSS when dragging
// because IE will ignore our custom "empty image" drag preview.
opacity: isDragging ? 0 : 1,
height: isDragging ? 0 : '',
}
}
export const DraggableBox = memo(function DraggableBox(props) {
const { id, title, left, top } = props
const [{ isDragging }, drag, preview] = useDrag(
() => ({
type: ItemTypes.BOX,
item: { id, left, top, title },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
}),
[id, left, top, title],
)
useEffect(() => {
preview(getEmptyImage(), { captureDraggingState: true })
}, [])
return (
<div
ref={drag}
style={getStyles(left, top, isDragging)}
role="DraggableBox"
>
<Box title={title} />
</div>
)
})
Here is my custom drag layer:
import { useDragLayer } from "react-dnd";
import { BoxDragPreview } from "./BoxDragPreview.js";
import { ItemTypes } from "./ItemTypes.js";
import { snapToGrid } from "./snapToGrid.js";
const layerStyles = {
position: "fixed",
pointerEvents: "none",
zIndex: 100,
left: 0 ,
top: 0 ,
width: "100%",
height: "100%",
};
function getItemStyles(initialOffset, currentOffset, isSnapToGrid) {
if (!initialOffset || !currentOffset) {
return {
display: 'none',
}
}
let { x, y } = currentOffset
if (isSnapToGrid) {
x -= initialOffset.x
y -= initialOffset.y
;[x, y] = snapToGrid(x, y)
x += initialOffset.x
y += initialOffset.y
}
const transform = ` translate(${x }px, ${y }px)`
return {
transform,
WebkitTransform: transform,
}
}
export const CustomDragLayer = (props) => {
const {
itemType,
isDragging,
item,
initialOffset,
currentOffset,
delta
} = useDragLayer((monitor) => ({
item: monitor.getItem(),
itemType: monitor.getItemType(),
initialOffset: monitor.getInitialSourceClientOffset(),
currentOffset: monitor.getSourceClientOffset(),
delta: monitor.getDifferenceFromInitialOffset(),
isDragging: monitor.isDragging()
}));
function renderItem() {
switch (itemType) {
case ItemTypes.BOX:
return <BoxDragPreview title={item.title} />;
default:
return null;
}
}
if (!isDragging) {
return null;
}
return (
<div style={layerStyles}>
<div style={getItemStyles(initialOffset, currentOffset, props.snapToGrid)}>{renderItem()}</div>
</div>
);
};
And here is my box drag preview:
import { memo, useEffect, useState } from "react";
import { Box } from "./Box.js";
const styles = {
display: "inline-block",
transform: "scale(.8)",
transformOrigin: "top left"
};
export const BoxDragPreview = memo(function BoxDragPreview({ title }) {
const [tickTock, setTickTock] = useState(false);
useEffect(
function subscribeToIntervalTick() {
const interval = setInterval(() => setTickTock(!tickTock), 500);
return () => clearInterval(interval);
},
[tickTock]
);
return (
<div style={styles}>
<Box title={title} yellow={tickTock} preview />
</div>
);
});
I tried item.left + delta.x / scaleValue, but that was even less accurate than this example. Here is the url to the codesandbox.
https://codesandbox.io/s/dazzling-wozniak-2fvs81?file=/src/BoxDragPreview.js:0-616
I am not able to get my "Messages" Drawer to display in safari when in mobile, but it is displaying just fine in Chrome. Below are images of what it looks like in the 2 browsers. I assume this is a CSS issue and that the bottom option bar is covering the button in Safari, but I am not sure what needs to change to fix this issue?
I am noticing it has to do with the height and bottom values in the css code, but not sure exactly what to change it to to work in both web browsers?
Safari
Chrome
Here is the code for the Message Drawer
import { createGesture, IonButton, IonCard } from "#ionic/react";
/* Core CSS required for Ionic components to work properly */
import "#ionic/react/css/core.css";
import dynamic from "next/dynamic";
import { useRouter } from "next/router";
import React, { useEffect, useRef, useState } from "react";
import { useGetThreadPageByIdQuery } from "../../../graphql/generated/component";
import { Request_Participant_Status_Value_Enum } from "../../../graphql/generated/graphql";
import ThreadMessages from "../../Messages/List";
import styles from "./styles.module.css";
let MessageWrapper = dynamic(
() => import("../../../components/Threads/MessageBox"),
{ ssr: false }
);
interface refType {
dataset: {
open: string;
};
style: {
transition: string;
transform: string;
};
}
const MessageDrawer = () => {
let drawerRef = useRef();
let router = useRouter();
let { threadID } = router.query;
let messageContainerRef = useRef(null);
let { data, loading } = useGetThreadPageByIdQuery({
variables: { thread_id: threadID },
nextFetchPolicy: "cache-and-network",
});
let scrollToLastMessage = () => {
let timer = setTimeout(() => {
messageContainerRef.current?.scrollIntoView({ beavior: "smooth" });
console.log("This will run after 1 second!");
console.log("message last scroll", data);
}, 500);
return () => clearTimeout(timer);
};
let [initialScrollCompleted, setInitialScrollComplete] =
useState<boolean>(false);
useEffect(() => {
if (initialScrollCompleted) {
return;
}
if (data) {
scrollToLastMessage();
return setInitialScrollComplete(true);
}
}, [loading, data]);
useEffect(() => {
let c: HTMLIonCardElement = drawerRef.current;
c.dataset.open = "false";
let gesture = createGesture({
el: c,
gestureName: "my-swipe",
direction: "y",
onMove: (event) => {
if (event.deltaY < -300) return;
// closing with a downward swipe
if (event.deltaY > 20) {
c.style.transform = "";
c.dataset.open = "false";
return;
}
c.style.transform = `translateY(${event.deltaY}px)`;
},
onEnd: (event) => {
c.style.transition = ".5s ease-out";
if (event.deltaY < -30 && c.dataset.open !== "true") {
c.style.transform = `translateY(${-62}vh) `;
c.dataset.open = "true";
console.log("in on end");
}
},
});
// enable the gesture for the item
gesture.enable(true);
}, []);
let toggleDrawer = () => {
let c: HTMLIonCardElement = drawerRef.current;
if (c) {
if (c.dataset.open === "true") {
c.style.transition = ".5s ease-out";
c.style.transform = "";
c.dataset.open = "false";
} else {
c.style.transition = ".5s ease-in";
c.style.transform = `translateY(${-62}vh) `;
c.dataset.open = "true";
}
}
};
return (
<IonCard className={styles.bottomdrawer} ref={drawerRef}>
<div className={styles.toggleMessage}>
<IonButton className={styles.toggleMessagebtn} onClick={toggleDrawer}>
<ion-icon
class={styles.icon}
slot="start"
src="./assets/dashboard/icons/messages_tab.svg"
/>
<ion-label>Messages</ion-label>
</IonButton>
</div>
<div className={styles.messageContent}>
<ion-list>
{/* <ion-item slot="header" class={styles.item}>
<ion-icon
class={styles.icon}
slot="start"
color="dark"
src="./assets/dashboard/icons/messages_tab.svg"
/>
<ion-label>Messages</ion-label>
</ion-item> */}
<div
slot="content"
className={`${styles.list} ${styles.commentsContainer}`}
>
<ThreadMessages threadID={threadID as string} />
<div
className={`${styles.messageContainerEnd}`}
ref={messageContainerRef}
/>
</div>
</ion-list>
<div className={styles.messageTypeContainer}>
{!data?.thread_by_pk?.request && data?.thread_by_pk?.breakdown ? (
<MessageWrapper
participants={data?.thread_by_pk.participants}
thread_id={threadID as string}
/>
) : null}
{!data?.thread_by_pk
?.request ? null : data?.thread_by_pk?.request?.request_participants?.find(
(request_participant) =>
request_participant.status ===
Request_Participant_Status_Value_Enum.Canceled
) ? null : (
<MessageWrapper
participants={data?.thread_by_pk.participants}
thread_id={threadID as string}
/>
)}
</div>
</div>
</IonCard>
);
};
export default MessageDrawer;
Here is the CSS Code for the Message Drawer.
.bottomdrawer {
position: fixed;
right: 0;
left: 0;
bottom: -71vh;
height: 74vh;
max-height: 100%;
border-radius: 10px 10px 0 0px;
top: auto;
margin: 0;
padding: 0;
-webkit-box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
overflow: hidden;
width: calc(100% - 30px);
margin: 0 auto;
z-index: 9999;
}
.bottomdrawer[data-open="true"] {
bottom: -79vh;
I suggest placing it in the IonFooter component, this way it can be sticky on the bottom of the page without you having to manually calculate the distance.
However if you want to use the CSS, you can try:
.bottomdrawer {
position: fixed;
right: 0;
left: 0;
bottom: calc(env(safe-area-inset-bottom)-71vh);
...
}
.bottomdrawer[data-open="true"] {
bottom: calc(env(safe-area-inset-bottom)-79vh);
I want to define the top property of a v-img depending on the current breakpoint of the window.
I wanted to define it like that:
<v-img contain id="logo-transparent" :top="logoTop" :width="logoWidth" :src="logoTransparent" class="hidden-xs-only"></v-img>
with the computed property looking like that:
logoTop(){
switch (this.$vuetify.breakpoint.name) {
case 'xl': return "-4%"
case 'lg': return "-6%"
case 'md': return "-8%"
case 'sm': return "-8%"
case 'xs': return 0
default:
return "-4%"
}
},
and the CSS like this:
#logo-transparent{
z-index: 1;
width: 400px;
height: 300px;
position: absolute;
right: -1%;
}
but the problem is that v-img doesn't have the top property.
I wanted to use my computed function to define the CSS of the image like this:
logoTop(){
return {
"--top-property" : switch (this.$vuetify.breakpoint.name) {
case 'xl': return 400
case 'lg': return 300
case 'md': return 300
case 'sm': return 200
case 'xs': return 0
default:
return 400
}
}
},
to be able to use it like this in the css:
top : var(--top-property)
but it seems that I can't use a switch in that case.
How could I do it?
switch doesn't return anything. You should use a variable like so
logoTop() {
let topProperty;
switch (this.$vuetify.breakpoint.name) {
case 'xl':
topProperty = 400;
break;
case 'lg':
case 'md':
topProperty = 300;
break;
case 'sm':
topProperty = 200;
break;
case 'xs':
topProperty = 0;
break;
default:
topProperty = 400;
}
return {
"--top-property" : topProperty
}
},
Your original logoTop computed property would work to set the v-img's top position in a style binding:
<template>
<v-img :style="{ top: logoTop }" ... />
</template>
<script>
export default {
computed: {
logoTop() {
switch (this.$vuetify.breakpoint.name) {
case 'xl': return "-4%"
case 'lg': return "-6%"
case 'md': return "-8%"
case 'sm': return "-8%"
case 'xs': return 0
default: return "-4%"
}
},
}
}
</script>
demo
Goal: I want to compare prevState with actualState to display a different class on a <div> depending on the comparision.
Problem: When the iteration results in no switch of class,i cant see the difference since the className doesnt trigger the keyframe event.
JSX:
import React, { useEffect, useState, useRef } from "react";
import styles from "./CoinContainer.module.css";
function usePrevious(data) {
const ref = useRef();
useEffect(() => {
ref.current = data;
}, [data]);
return ref.current;
}
export default function CoinContainer({ coin, price }) {
const [priceUpdated, setPrice] = useState("");
const prevPrice = usePrevious(priceUpdated);
useEffect(() => {
setInterval(priceUpdate, 20000);
});
const toggleClassName = () => {
return prevPrice > priceUpdated ? styles.redPrice : styles.greenPrice;
};
function priceUpdate() {
return fetch(
`https://api.coingecko.com/api/v3/simple/price?ids=${coin}&vs_currencies=usd`
)
.then((data) => data.json())
.then((result) => {
let key = Object.keys(result);
setPrice(result[key].usd);
});
}
return (
<div className={styles.padding}>
<h2>{coin}</h2>
<h3 className={toggleClassName()}>
{priceUpdated ? priceUpdated : price}$
</h3>
</div>
);
}
CSS:
#keyframes upFadeBetween {
from {
color: green;
}
to {
color: white;
}
}
#keyframes downFadeBetween {
from {
color: red;
}
to {
color: white;
}
}
.redPrice {
color: white;
animation: downFadeBetween 5s;
}
.greenPrice {
color: white;
animation: upFadeBetween 5s;
}
Thanks so much for any feedback/help!