Reputation: 21
I've been using React for some time but know I want to improve my knowledge to another level so I want to know what would be your approach for managing the state on this kind of application?
import { Pedalboard } from "../../Components/Pedalboard/Pedalboard";
import { PedalboardOptions } from "../../Components/PedalboardOptions/PedalboardOptions";
import { exampleData } from "./exampleData";
import { Style } from "./PedalboardView.css";
import { useWindowSize } from "../../Hooks";
import { DndProvider } from "react-dnd";
import MultiBackend from "react-dnd-multi-backend";
import HTML5toTouch from "react-dnd-multi-backend/dist/esm/HTML5toTouch";
export const PedalboardView = () => {
let windowSize = useWindowSize();
const bodyRef = useRef();
const [pedalboardData, setPedalboardData] = useState(
JSON.parse(localStorage.getItem("pedalboardData"))
? JSON.parse(localStorage.getItem("pedalboardData"))
: exampleData
);
const [scale, setScale] = useState(
JSON.parse(localStorage.getItem("scale"))
? JSON.parse(localStorage.getItem("scale"))
: 18
);
const [pbAreaSize, setPbAreaSize] = useState(
JSON.parse(localStorage.getItem("pbAreaSize"))
? JSON.parse(localStorage.getItem("pbAreaSize"))
: { width: 60, height: 30 }
);
const [htmlDrag, setHtmlDrag] = useState(true);
//Temporary options
const [fitToWidth, setFitToWidth] = useState(false);
const [fitToHeight, setFitToHeight] = useState(false);
const [hideOptions, setHideOptions] = useState(false);
const [showTransitions, setShowTransitions] = useState(false);
const [autofillEmpty, setAutofillEmpty] = useState(false);
const [unitFactor, setUnitFactor] = useState("1");
const [pbScrollBarSize, setPbScrollBarSize] = useState({
width: 0,
height: 0,
});
const actualElement = useRef();
//We send the available dimensions to the child from here to have it before in case we need
//to calculate percentages and animations
let availableWidth =
windowSize !== undefined
? hideOptions
? windowSize.width
: windowSize.width * 0.8
: "";
let availableHeight = windowSize !== undefined ? windowSize.height - 50 : "";
const preSetScale = (newScale) => {
// When the scale changes the elements positions are recalculated
let aux2 = { ...pedalboardData };
Object.keys(pedalboardData).map((key) => {
aux2[key].left = (aux2[key].left * newScale) / scale;
aux2[key].top = (aux2[key].top * newScale) / scale;
});
localStorage.setItem("scale", JSON.stringify(newScale));
setPedalboardData(aux2);
setScale(newScale);
// eslint-disable-next-line react-hooks/exhaustive-deps
};
const fillEmptySpace = (type = "both") => {
setPbAreaSize({
width:
(type === "width" || type === "both") &&
pbAreaSize.width < availableWidth / scale
? availableWidth / scale - pbScrollBarSize.width / scale
: pbAreaSize.width,
height:
(type === "height" || type === "both") &&
pbAreaSize.height < availableHeight / scale
? availableHeight / scale - pbScrollBarSize.height / scale
: pbAreaSize.height,
});
};
//Local storage saving
useEffect(() => {
localStorage.setItem("pbAreaSize", JSON.stringify(pbAreaSize));
}, [pbAreaSize]);
useEffect(() => {
localStorage.setItem("pedalboardData", JSON.stringify(pedalboardData));
}, [pedalboardData]);
useEffect(() => {
localStorage.setItem("scale", JSON.stringify(scale));
}, [scale]);
//Effects
useEffect(() => {
if (fitToWidth) {
preSetScale((availableWidth - pbScrollBarSize.width) / pbAreaSize.width);
}
if (fitToHeight) {
preSetScale(
(availableHeight - pbScrollBarSize.height) / pbAreaSize.height
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
fitToWidth,
fitToHeight,
pbAreaSize.width,
availableWidth,
pbAreaSize.height,
availableHeight,
]);
useEffect(() => {
if (autofillEmpty) {
if (fitToWidth) {
setPbAreaSize({
...pbAreaSize,
height:
true && pbAreaSize.height < availableHeight / scale
? availableHeight / scale - pbScrollBarSize.height / scale
: pbAreaSize.height,
});
} else {
setPbAreaSize({
width:
true && pbAreaSize.width < availableWidth / scale
? availableWidth / scale - pbScrollBarSize.width / scale
: pbAreaSize.width,
height:
true && pbAreaSize.height < availableHeight / scale
? availableHeight / scale - pbScrollBarSize.height / scale
: pbAreaSize.height,
});
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [availableWidth]);
useEffect(() => {
if (autofillEmpty) {
fillEmptySpace();
}
}, [scale, autofillEmpty]);
return (
<div css={Style(hideOptions)} ref={bodyRef}>
<div className="headSec">Head</div>
<div className="bodySec">
<div className="pbZone">
<DndProvider backend={MultiBackend} options={HTML5toTouch}>
<Pedalboard
className={""}
scale={scale}
pbAreaSize={pbAreaSize}
showTransitions={showTransitions}
setShowTransitions={setShowTransitions}
setPbScrollBarSize={setPbScrollBarSize}
setPbAreaSize={setPbAreaSize}
pedalboardData={pedalboardData}
availableWidth={availableWidth}
availableHeight={availableHeight}
setPedalboardData={setPedalboardData}
actualElement={actualElement}
htmlDrag={htmlDrag}
unitFactor={unitFactor}
/>
</DndProvider>
</div>
<PedalboardOptions
className={"pbOptions"}
scale={scale}
setScale={preSetScale}
pbAreaSize={pbAreaSize}
setPbAreaSize={setPbAreaSize}
fitToWidth={fitToWidth}
setFitToWidth={setFitToWidth}
fitToHeight={fitToHeight}
setFitToHeight={setFitToHeight}
hideOptions={hideOptions}
setHideOptions={setHideOptions}
setShowTransitions={setShowTransitions}
autofillEmpty={autofillEmpty}
setAutofillEmpty={setAutofillEmpty}
pedalboardData={pedalboardData}
setPedalboardData={setPedalboardData}
htmlDrag={htmlDrag}
setHtmlDrag={setHtmlDrag}
fillEmptySpace={fillEmptySpace}
unitFactor={unitFactor}
setUnitFactor={setUnitFactor}
actualElement={actualElement}
/>
</div>
</div>
);
};
I've done some research on useState vs useReducer but i really dont know what approach would be the best, some people said that is best to use useState when the properties change independent and useReducer when some action change more of one at the same time. Looking at my functions, only some of them update 2 states at the same time so I don't know if changing all of my state to useReducer would be worth it or what would be your approach to this kind of aplication?
This is preview of what im doing (a pedalboard planer for guitarists)
Here is the source code of my project: https://github.com/ricardoaxel/pedalvisioncore/tree/master/pedalvision/src
And other files if you dont want to enter to github: Pedalboard.jsx
import React, { useRef, useEffect, useState, useCallback } from "react";
import pedals from "../../utils/pedals.json";
import pedalboards from "../../utils/pedalboards.json";
import { PBElement } from "../PBElement/PBElement";
import { getLatestPositions } from "../../utils/functions/getLatestsPositions";
import { useDrop } from "react-dnd";
import update from "immutability-helper";
export const Pedalboard = ({
className,
scale,
pbAreaSize,
showTransitions,
setShowTransitions,
setPbScrollBarSize,
setPbAreaSize,
pedalboardData,
setPedalboardData,
actualElement,
htmlDrag,
unitFactor,
}) => {
const localRef = useRef();
const movePedal = (direction, num) => {
let auxPB = { ...JSON.parse(localStorage.getItem("pedalboardData")) };
if (
auxPB[actualElement.current.id][direction] +
num * JSON.parse(localStorage.getItem("scale")) <
0
) {
num = 0;
}
let isHorizontal =
Math.abs(actualElement.current.particularInfo.orientation) === 0 ||
Math.abs(actualElement.current.particularInfo.orientation) === 180;
if (
auxPB[actualElement.current.id][direction] +
num * JSON.parse(localStorage.getItem("scale")) +
actualElement.current.elTypeInfo[
direction === "top"
? isHorizontal
? "Height"
: "Width"
: isHorizontal
? "Width"
: "Height"
] *
JSON.parse(localStorage.getItem("scale")) >
JSON.parse(localStorage.getItem("pbAreaSize"))[
direction === "top" ? "height" : "width"
] *
JSON.parse(localStorage.getItem("scale"))
) {
num = 0;
}
auxPB[actualElement.current.id][direction] =
auxPB[actualElement.current.id][direction] +
num * JSON.parse(localStorage.getItem("scale"));
setPedalboardData({ ...auxPB });
};
const escFunction = (event) => {
if (actualElement.current !== undefined) {
switch (event.key) {
case "ArrowLeft":
movePedal("left", -1);
break;
case "ArrowRight":
movePedal("left", 1);
break;
case "ArrowUp":
movePedal("top", -1);
break;
case "ArrowDown":
movePedal("top", 1);
break;
default:
break;
}
}
};
useEffect(() => {
setPbScrollBarSize({
width: localRef.current.offsetWidth - localRef.current.clientWidth,
height: localRef.current.offsetHeight - localRef.current.clientHeight,
});
document.addEventListener("keydown", escFunction, false);
}, []);
const deletePBElement = (id) => {
let auxPBData = { ...pedalboardData };
delete auxPBData[id];
setPedalboardData({ ...auxPBData });
};
const rotatePBElement = (id, deg) => {
let auxPB = { ...pedalboardData };
auxPB[id]["orientation"] =
parseInt(auxPB[id]["orientation"]) + deg >= 360 ||
parseInt(auxPB[id]["orientation"]) + deg <= -360
? 0
: parseInt(auxPB[id]["orientation"]) + deg;
let auxSize = {
width: getLatestPositions(auxPB, scale, "width") / scale + 1,
height: getLatestPositions(auxPB, scale, "height") / scale + 1,
};
setPbAreaSize({
width:
pbAreaSize.width > auxSize.width ? pbAreaSize.width : auxSize.width,
height:
pbAreaSize.height > auxSize.height ? pbAreaSize.height : auxSize.height,
});
setPedalboardData({ ...auxPB });
};
const updateElementLayer = (id, num) => {
let auxPB = { ...pedalboardData };
let newNum = parseInt(auxPB[id]["layer"]) + num;
auxPB[id]["layer"] = newNum < 1 ? 1 : newNum > 10 ? 10 : newNum;
setPedalboardData({ ...auxPB });
};
const moveBox = useCallback(
(id, left, top, elementTypeInfo) => {
//Validations to avoid image traspasing available area
if (left < 0) {
left = 0;
}
if (top < 0) {
top = 0;
}
let isHorizontal =
Math.abs(pedalboardData[id].orientation) === 0 ||
Math.abs(pedalboardData[id].orientation) === 180;
if (
left + elementTypeInfo[isHorizontal ? "Width" : "Height"] * scale >
pbAreaSize.width * scale
) {
left =
pbAreaSize.width * scale -
elementTypeInfo[isHorizontal ? "Width" : "Height"] * scale;
}
if (
top + elementTypeInfo[isHorizontal ? "Height" : "Width"] * scale >
pbAreaSize.height * scale
) {
top =
pbAreaSize.height * scale -
elementTypeInfo[isHorizontal ? "Height" : "Width"] * scale;
}
setPedalboardData(
update(pedalboardData, {
[id]: {
$merge: { left, top },
},
})
);
},
[pedalboardData]
);
const [, drop] = useDrop(
() => ({
accept: "box",
drop(item, monitor) {
const delta = monitor.getDifferenceFromInitialOffset();
const left = Math.round(item.left + delta.x);
const top = Math.round(item.top + delta.y);
moveBox(item.id, left, top, item.elementTypeInfo);
return undefined;
},
}),
[moveBox]
);
return (
<div
css={Style(
pbAreaSize.width,
pbAreaSize.height,
scale,
localRef.current &&
pbAreaSize.width * scale + 1 < localRef.current.clientWidth &&
localRef.current &&
pbAreaSize.height * scale + 1 <= localRef.current.clientHeight,
unitFactor
)}
className={className}
ref={localRef}
>
<div ref={drop} className="pedalboardAreaContainer">
<div className="gridArea"></div>
{Object.keys(pedalboardData).map((key) => {
const { left, top, title } = pedalboardData[key];
let locOtherData = pedalboardData[key];
let elementTypeInfo;
if (locOtherData.type === "pedals") {
elementTypeInfo = pedals.filter(
(pedal) =>
pedal.Name === locOtherData.Name &&
pedal.Brand === locOtherData.Brand
)[0];
} else {
elementTypeInfo = pedalboards.filter(
(pedal) =>
pedal.Name === locOtherData.Name &&
pedal.Brand === locOtherData.Brand
)[0];
}
return (
<PBElement
key={key}
id={key}
left={left}
top={top}
hideSourceOnDrag={true}
otherData={pedalboardData[key]}
elementTypeInfo={elementTypeInfo}
scale={scale}
showTransitions={showTransitions}
setShowTransitions={setShowTransitions}
rotatePBElement={rotatePBElement}
deletePBElement={deletePBElement}
updateElementLayer={updateElementLayer}
setActualElement={(val) => (actualElement.current = val)}
htmlDrag={htmlDrag}
handleEvent={moveBox}
>
{title}
</PBElement>
);
})}
</div>
</div>
);
};
PedalboardOptions.jsx
import { Style } from "./PedalboardOptions.css";
import React, { useState } from "react";
import pedals from "../../utils/pedals.json";
import pedalboards from "../../utils/pedalboards.json";
import { getLatestPositions } from "../../utils/functions/getLatestsPositions";
export const PedalboardOptions = ({
className,
scale,
setScale,
pbAreaSize,
setPbAreaSize,
fitToWidth,
setFitToWidth,
fitToHeight,
setFitToHeight,
hideOptions,
setHideOptions,
setShowTransitions,
autofillEmpty,
setAutofillEmpty,
pedalboardData,
setPedalboardData,
htmlDrag,
setHtmlDrag,
fillEmptySpace,
unitFactor,
setUnitFactor,
}) => {
const addElement = (elementIndex, type) => {
let elementTypeInfo;
if (type === "pedals") {
elementTypeInfo = pedals[elementIndex];
} else {
elementTypeInfo = pedalboards[elementIndex];
}
let auxObj = {
left: 0,
top: 0,
type: type,
Name: elementTypeInfo.Name,
Brand: elementTypeInfo.Brand,
orientation: 0,
//Obtaining the last layer
layer: Math.max(...Object.values(pedalboardData).map((el) => el.layer)),
};
//This validation change the size of the actual area to work in case the element doesn't fit
let auxPB = { ...pedalboardData };
auxPB[Math.random().toString(16).slice(2)] = auxObj;
let changeSize = false;
let auxNewSize = { ...pbAreaSize };
if (elementTypeInfo.Width * scale + 5 > pbAreaSize.width * scale) {
auxNewSize = {
...auxNewSize,
width: elementTypeInfo.Width + 10,
};
changeSize = true;
}
if (elementTypeInfo.Height * scale + 5 > pbAreaSize.height * scale) {
auxNewSize = {
...auxNewSize,
height: elementTypeInfo.Height + 10,
};
changeSize = true;
}
if (changeSize) {
setPbAreaSize(auxNewSize);
}
setPedalboardData(auxPB);
};
const changeLayoutSize = (value, type) => {
let maxOfType = getLatestPositions(pedalboardData, scale, type);
if (value > maxOfType / scale) {
setPbAreaSize({ ...pbAreaSize, [type]: value });
}
};
const adjustLayoutToElements = (type = "both") => {
//The use of unitFactor is to adjust to the actual type of units
setPbAreaSize({
width:
type === "width" || type === "both"
? Math.floor(
(getLatestPositions(pedalboardData, scale, "width") / scale + 1) *
unitFactor
) / unitFactor
: pbAreaSize.width,
height:
type === "height" || type === "both"
? Math.floor(
(getLatestPositions(pedalboardData, scale, "height") / scale +
1) *
unitFactor
) / unitFactor
: pbAreaSize.height,
});
};
const [hideElements, setHideElements] = useState(true);
return (
<div
css={Style()}
className={className}
onMouseEnter={() => {
setShowTransitions(true);
setHideElements(false);
}}
onClick={() => setShowTransitions(true)}
onMouseLeave={() => setHideElements(true)}
>
<div className="elementsAddSection">
<label>
<input
type="checkbox"
checked={htmlDrag}
onChange={() => setHtmlDrag(!htmlDrag)}
/>{" "}
HTML 5 Dnd
</label>
<br />
<label>
<input
type="checkbox"
checked={fitToWidth}
onChange={() => {
setFitToWidth(!fitToWidth);
setFitToHeight(false);
}}
/>{" "}
Fit to View width
</label>
<br />
<label>
<input
type="checkbox"
checked={fitToHeight}
onChange={() => {
setFitToHeight(!fitToHeight);
setFitToWidth(false);
}}
/>{" "}
Fit to View height
</label>
<br />
Scale (representation of inches per pixel):{scale}
<input
type="number"
name="lastName"
value={scale}
onChange={(e) => setScale(e.target.value)}
disabled={fitToWidth || fitToHeight}
/>
<br />
Units:{scale}
<br />
<label>
<input
type="checkbox"
checked={unitFactor === "1"}
onChange={() => setUnitFactor("1")}
/>{" "}
in
</label>
<label>
<input
type="checkbox"
checked={unitFactor === "2.54"}
onChange={() => setUnitFactor("2.54")}
/>{" "}
cm
</label>
<br />
Layout size: <br />
Adjust to last elements:
<br />
<button onClick={() => adjustLayoutToElements()}>Both</button>
<button onClick={() => adjustLayoutToElements("width")}>Width</button>
<button onClick={() => adjustLayoutToElements("height")}>Height</button>
<br />
Fill empty space:
<br />
<button onClick={() => fillEmptySpace()}>Both</button>
<button onClick={() => fillEmptySpace("width")}>Width</button>
<button onClick={() => fillEmptySpace("height")}>Height</button>
<br />
<label>
<input
type="checkbox"
value={autofillEmpty}
onChange={() => setAutofillEmpty(!autofillEmpty)}
/>{" "}
Autofill empty space
<br />
</label>
Width:
<input
type="number"
name="lastName"
value={(pbAreaSize.width * unitFactor).toFixed(2)}
onChange={(e) => {
changeLayoutSize(e.target.value / unitFactor, "width");
}}
/>
<br />
Height:
<input
type="number"
name="lastName"
value={(pbAreaSize.height * unitFactor).toFixed(2)}
onChange={(e) =>
changeLayoutSize(e.target.value / unitFactor, "height")
}
/>
{(!hideElements || htmlDrag) && (
<>
<div className="elementSel">
Pedalboards:
<select
onChange={(e) => addElement(e.target.value, "pedalboards")}
>
{pedalboards.map((pedalboard, index) => (
<option key={index} value={index}>
{pedalboard.Name}
</option>
))}
</select>
</div>
<div className="elementSel">
Pedals:
<select onChange={(e) => addElement(e.target.value, "pedals")}>
{pedals.map((pedal, index) => (
<option key={index} value={index}>
{pedal.Name}
</option>
))}
</select>
</div>
</>
)}
</div>
<div className="toggleBtn" onClick={() => setHideOptions(!hideOptions)}>
{">"}
</div>
</div>
);
};
PBElement.jsx
import { useState } from "react";
import { useDrag } from "react-dnd";
import { Style } from "./PBElement.css";
import Draggable from "react-draggable";
export const PBElement = ({
id,
left,
top,
hideSourceOnDrag,
otherData,
elementTypeInfo,
scale,
showTransitions,
setShowTransitions,
rotatePBElement,
deletePBElement,
updateElementLayer,
setActualElement,
htmlDrag,
handleEvent,
}) => {
const [hideOptions, setHideOptions] = useState(false);
const [{ isDragging }, drag] = useDrag(
() => ({
type: "box",
item: { id, left, top, elementTypeInfo },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
}),
[id, left, top]
);
if (isDragging && hideSourceOnDrag) {
return <div ref={drag} />;
}
if (htmlDrag) {
return (
<div
css={Style(
elementTypeInfo.Width,
elementTypeInfo.Height,
scale,
showTransitions,
otherData,
hideOptions
)}
style={{ left: left, top: top, touchAction: "none" }}
ref={drag}
onMouseLeave={() => setHideOptions(false)}
onDragStart={() => setHideOptions(true)}
// onDragLeave={() => setHideOptions(false)}
// onDragEnd={() => setHideOptions(false)}
onClick={() =>
setActualElement({
id: id,
particularInfo: otherData,
elTypeInfo: elementTypeInfo,
})
}
>
<img
className={"elementImage"}
src={require(`../../assets/Images/${otherData.type}/${elementTypeInfo.Image}`)}
alt=""
/>
<div className="borderSquare" draggable="false">
<div className={`options `}>
<p onClick={() => rotatePBElement(id, -90)}>{"<-"}</p>
<p onClick={() => deletePBElement(id)}>X</p>
<p onClick={() => rotatePBElement(id, 90)}>{"->"}</p>
</div>
</div>
<div className={`layer `} draggable="false">
<p onClick={() => updateElementLayer(id, 1)}>{"A"}</p>
<p>{otherData.layer}</p>
<p onClick={() => updateElementLayer(id, -1)}>{"V"}</p>
</div>
</div>
);
} else {
return (
<Draggable
position={{
x: left,
y: top,
}}
key={id}
bounds="parent"
onStop={(e, data) =>
handleEvent(id, data.lastX, data.lastY, elementTypeInfo)
}
onStart={() => setShowTransitions(false)}
// onDrag={() => setShowTransitions(false)}
draggable="false"
>
<div
css={Style(
elementTypeInfo.Width,
elementTypeInfo.Height,
scale,
showTransitions,
otherData,
false
)}
draggable="false"
onClick={() => {
setActualElement({
id: id,
particularInfo: otherData,
elTypeInfo: elementTypeInfo,
});
setShowTransitions(true);
}}
>
<img
className={"elementImage"}
src={require(`../../assets/Images/${otherData.type}/${elementTypeInfo.Image}`)}
//To avoid the default HTML5 drag API
draggable="false"
alt=""
/>
<div className="borderSquare" draggable="false">
<div className={`options `}>
<p onClick={() => rotatePBElement(id, -90)}>{"<-"}</p>
<p onClick={() => deletePBElement(id)}>X</p>
<p onClick={() => rotatePBElement(id, 90)}>{"->"}</p>
</div>
</div>
<div className={`layer `} draggable="false">
<p onClick={() => updateElementLayer(id, 1)}>{"A"}</p>
<p>{otherData.layer}</p>
<p onClick={() => updateElementLayer(id, -1)}>{"V"}</p>
</div>
</div>
</Draggable>
);
}
};
Upvotes: 0
Views: 104
Reputation:
Just for a start, take a look at these useState
calls:
const [pedalboardData, setPedalboardData] = useState(
JSON.parse(localStorage.getItem("pedalboardData"))
? JSON.parse(localStorage.getItem("pedalboardData"))
: exampleData
);
const [scale, setScale] = useState(
JSON.parse(localStorage.getItem("scale"))
? JSON.parse(localStorage.getItem("scale"))
: 18
);
const [pbAreaSize, setPbAreaSize] = useState(
JSON.parse(localStorage.getItem("pbAreaSize"))
? JSON.parse(localStorage.getItem("pbAreaSize"))
: { width: 60, height: 30 }
);
It's obviously redundant and unnecessary so let's make this a reusable hook instead:
function useLocalStorage(key, defaultTo) {
return useState(
localStorage.getItem(key)
? JSON.parse(localStorage.getItem(key))
: defaultTo
);
}
And now when we use it we can see it's much cleaner:
const [pedalboardData, setPedalboardData] = useLocalStorage("pedalboardData", exampleData);
const [scale, setScale] = useLocalStorage("scale", 18);
const [pbAreaSize, setPbAreaSize] = useLocalStorage("pbAreaSize", { width: 60, height: 30 });
And another thing you might want to do is namespace your local storage keys. Keys like scale
are too common and might get overriden/read by other sites.
You could try something like whatever-my-app-name-is-scale
instead.
Upvotes: 1