I've came across this, which helped me this as far as I've got. Though, I'm trying to hack together a simple status component that instead of a checkbox, is just a div with styling to make it appear as a tiny dot that toggles between three strings, offline, wip and online onClick! Changing just the color upon change of state. (Practically speaking, I'll set an array of objects as offline and if toggled differently I'll store that preference.)
I'm just stuck trying to move away from a checkbox, I'll show you what I mean:
const STATUS_STATES = {
Online: "online",
Wip: "wip",
Offline: "offline",
};
function SomePage() {
const [status, setStatus] = useState(STATUS_STATES.Offline);
const handleChange = () => {
let updatedChecked;
if (status === STATUS_STATES.Online) {
updatedChecked = STATUS_STATES.Offline;
} else if (status === STATUS_STATES.Offline) {
updatedChecked = STATUS_STATES.Wip;
} else if (status === STATUS_STATES.Wip) {
updatedChecked = STATUS_STATES.Online;
}
setStatus(updatedChecked);
};
const Status = ({ value, onChange }) => {
const checkboxRef = React.useRef();
useEffect(() => {
if (value === STATUS_STATES.Online) {
console.log("online")
checkboxRef.current.checked = true;
checkboxRef.current.indeterminate = false;
} else if (value === STATUS_STATES.Offline) {
console.log("offline")
checkboxRef.current.checked = false;
checkboxRef.current.indeterminate = false;
} else if (value === STATUS_STATES.Wip) {
console.log("wip")
checkboxRef.current.checked = false;
checkboxRef.current.indeterminate = true;
}
}, [value]);
return (
<label>
<input ref={checkboxRef} type="checkbox" onChange={onChange} />
{/* I need to replace the line above with the line below */}
{/* while escaping the label element it's wrapped within */}
<div
className={
(value === "offline" && "offline") ||
(value === "wip" && "wip") ||
(value === "online" && "online")
}
onChange={onChange}
/>
</label>
);
};
return (
<>
<Status value={status} onChange={handleChange} />
<p>Is checked? {status}</p>
</>
)
}
.status{
width: 6px;
height: 6px;
background: #ee4f4f;
border-radius: 50%;
}
Any advice to approach this more efficiently?
This tidies it up quite a bit
codebox: https://codesandbox.io/s/intelligent-gareth-7qvko?file=/src/App.js:0-1050
import "./styles.css";
import React, { useState, useEffect } from "react";
const STATUS_STATES = ["Online", "Wip", "Offline"];
const STATUS_COLORS = ["green", "orange", "red"];
const Dot = ({ color, onClick }) => (
<div
onClick={onClick}
style={{ borderRadius: 5, height: 10, width: 10, background: color }}
/>
);
const Main = () => {
const [status, setStatus] = useState(0);
const handleClick = () => {
const newStatus = (status + 1) % STATUS_STATES.length;
setStatus(newStatus);
};
const text = STATUS_STATES[status];
return (
<>
<Dot onClick={handleClick} color={STATUS_COLORS[status]} />
<div onClick={handleClick}>{text}</div>
</>
);
};
export default Main;
You will see to loop through the statuses, I have put them through in an array, and used the remainder operator to get the next index.
The useEffect logic only needed a simple if for each value which has been written in short hand which (I think) is more readable for variables.
You will also notice the onclick on the checkbox input is wrapped in a timeout with a 0ms wait. This is a workaround because I couldnt prevent the default checkbox behaviour from happening. This ensures the click handling is run after the native logic.
I also reduced your array to just an array of strings - This was just to simplify the example.
Related
I have been struggling with this for hours, i'm new to React and would appreciate any assistance.
I'm working on something where users can pick regions into an array.
My main problem is that i want the array that users choose to have unique values only.
I have tried using a javascript SET but that can't be mapped through. The array will be mapped through then displayed to the user.
And i have tried setting "if" statements, that check for duplicate values, inside useEffect but the dependency on a useState array creates an infinite loop.
I have read about using useRef on an array to avoid useEffect infinite loops but i find that its normally for static rather than changing arrays.
Below is the important part:
const [regions, setRegions] = useState([]);
const [region, setRegion] = useState("");
useEffect(() => {
if (region) {
if (regions.includes.region) {
return;
} else if (!regions.includes.region) {
setRegions((prevValue) => {
return [...prevValue, region];
});
}
}
// setRegions((previousState) => new Set([...previousState, region]));
}, [region, regions]);
The rest of the code for context:
import { useEffect, useState } from "react";
import PlacesAutocomplete, {
geocodeByAddress,
getLatLng,
} from "react-places-autocomplete";
export default function Test() {
const [address, setAddress] = useState("");
const [coordinate, setCoordinates] = useState({
lat: null,
lng: null,
});
const [regions, setRegions] = useState([]);
const [region, setRegion] = useState("");
useEffect(() => {
if (region) {
if (regions.includes.region) {
return;
} else if (!regions.includes.region) {
setRegions((prevValue) => {
return [...prevValue, region];
});
}
}
// setRegions((previousState) => new Set([...previousState, region]));
}, [region, regions]);
const handleSelect = async (value) => {
const result = await geocodeByAddress(value);
const full_region = result[0].formatted_address;
const part_region = full_region.substring(0, full_region.indexOf(","));
let province = "";
if (result[0].address_components[2].short_name.length <= 3) {
province = result[0].address_components[2].short_name;
} else {
province = result[0].address_components[3].short_name;
}
setAddress(value);
setCoordinates(coordinate);
setRegion(part_region.concat("-", province));
};
const onDelete = (e) => {
const value = e.target.getAttribute("value");
console.log("onDelete: ", value);
setRegions(regions.filter((item) => item !== value));
};
// setRegions(Array.from(new Set(regions)));
return (
<>
<PlacesAutocomplete
value={address}
onChange={setAddress}
onSelect={handleSelect}
searchOptions={{
componentRestrictions: { country: ["za"] },
types: ["(regions)"],
}}
>
{({ getInputProps, suggestions, getSuggestionItemProps, loading }) => (
<div>
<input
{...getInputProps({
placeholder: "Add regions...",
className: "location-search-input",
})}
/>
<div className="autocomplete-dropdown-container">
{loading && <div>Loading...</div>}
{suggestions.map((suggestion) => {
const className = suggestion.active
? "suggestion-item--active"
: "suggestion-item";
// inline style for demonstration purpose
const style = suggestion.active
? { backgroundColor: "orange", cursor: "pointer" }
: { backgroundColor: "silver", cursor: "pointer" };
return (
<div
key={suggestion.description}
{...getSuggestionItemProps(suggestion, {
className,
style,
})}
>
<span>{suggestion.description}</span>
</div>
);
})}
</div>
</div>
)}
</PlacesAutocomplete>
<p>Regions</p>
<ul>
{regions.map((region) => (
<li
// key={region}
title="remove"
className="cursor-pointer"
onClick={onDelete}
value={region}
>
{region}
</li>
))}
</ul>
</>
);
}
You are using .includes() incorrectly by trying to obtain region as a property: regions.includes.region
This results in:
the second condition else if (!regions.includes.region) always succeeding,
which then results in the state change setRegions() being made,
which then triggers the [regions] in the dependency,
which then loops the useEffect() again, and again.. ..infinitely.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes
Instead, it should be passed as a parameter to the method: if(regions.includes(region)) and if(!regions.includes(region))
useEffect(() => {
if (region) {
if (regions.includes(region)) {
return;
}
if (!regions.includes(region)) {
setRegions((prevValue) => {
return [...prevValue, region];
});
}
}
}, [region, regions]);
You could probably also simplify it by only modifying the state if the condition doesn't succeed:
useEffect(() => {
if (region) {
if (!regions.includes(region)) {
setRegions((prevValue) => {
return [...prevValue, region];
});
}
// else do nothing
}
}, [region, regions]);
I am trying to perform an animation fadein and fadeout to the images in a gallery with framer motions variants. The problem is I didn't manage to trigger the second animation.
So far, I have created a holder for the animation to be displayed till the image load, which I call DotLottieWrapper. I trigger the animation of this holder in the first render and when the loading state changes but the second looks like is not happening. My code is bellow:
const [loading,setLoading] = useState(1);
const loadedDone = () => {
const animationTime = 6000
async function wait() {
await new Promise(() => {
setTimeout(() => {
setLoading(0)
}, animationTime);
})
};
wait();
}
const StyledWrapper = wrapperStyle ?
styled(wrapperStyle)`position:relative;` :
styled(imageStyle)`
${cssImageStyle(src)}
`;
const animationVariants = {
0:{
backgroundColor:'#300080',
opacity:0,
transition:{duration:1.5}
},
1:{
opacity:1,
backgroundColor:'#702090',
transition:{duration:1.5}
}
}
return (
<StyledWrapper
as={NormalDiv}
onClick={handleClick || function () { }}
>
{(loading < 2) && (
<DotLottieWrapper
initial='0'
animate={loading===1?'1':'0'}
variants={animationVariants}
key={IDGenerator()}
>
</DotLottieWrapper>
)}
{(loading < 2) && (
<img
key={IDGenerator()}
onLoad={() => {
loadedDone();
}}
src={src}
/>
)}
</StyledWrapper>
)
Have I done something wrong?
I'm trying to implement React version of TextArea which appends "$" every-time I press Return/Enter.
I'm having hard time prepending a sign # or % or dollar every time someone presses enter. How can I go about this?
This is my basic attempt but I'm kind of lost:
const MyComponent = () => {
const [name, setName] = React.useState('');
return (
<div>
<textarea value={name} onChange={(e) => { setName(e.target.value) }} />
</div>
);
}
ReactDOM.render(<MyComponent />, document.getElementById('root'));
Ok so I had a bit of time on my hands and thought this could be a good learning experience. So I present to you: MoneyInputList
import './css/MoneyInputList.css'
import { useState, useEffect } from 'react';
let latestAdded = 'moneyInputList-input-0';
let lastSize = 0;
const MoneyInputList = () => {
const [recordList, setRecordList] = useState({data: ['']});
const handleFormSubmit = (e) => {
e.preventDefault();
if(recordList.data[recordList.data.length-1] !== ''){
setRecordList({data: [...recordList.data, '']})
}
};
useEffect(() => {
if(lastSize !== recordList.data.length)
document.getElementById(latestAdded).focus();
lastSize = recordList.data.length;
}, [recordList]);
return (
<form autoComplete='off' onSubmit={handleFormSubmit}>
<div className="main-container">
{recordList.data.length > 0 &&
recordList.data.map((record, iter) => {
latestAdded = "moneyInputList-input-"+iter;
return (
<div key={"moneyInputList-field-"+iter} className="record-field">
<div className="record-sign">$</div>
<input className="record-input" id={"moneyInputList-input-"+iter} value={recordList.data[iter]} onChange={(e) => {
if(e.target.value === '' && iter !== recordList.data.length-1){
let modifiedData = [];
recordList.data.forEach((e,i) => {
if(i !== iter)
modifiedData.push(e);
});
setRecordList({data: modifiedData});
return;
}
const filteredValue = e.target.value.split('').filter(e=>(e.charCodeAt() >= '0'.charCodeAt() && e.charCodeAt() <= '9'.charCodeAt()));
let formattedValue = [];
filteredValue.forEach((elem, i) => {
if((filteredValue.length - i) % 3 === 0 && i !== 0)
formattedValue.push(',');
formattedValue.push(elem);
});
formattedValue = formattedValue.join('');
e.target.value = formattedValue;
let myData = recordList.data;
myData[iter] = e.target.value;
setRecordList({data: myData});
}} type="text"/>
</div>
)})
}
</div>
<input style={{flex: 0, visibility: 'collapse', height: 0, width: 0, padding: 0, margin: 0}} type="submit"/>
</form>
)
}
export default MoneyInputList;
This component should do what you need it to do. It is not the best code but it works. You can see it working here. Of course you might still need to change some stuff in order for it to fit in your codebase and maybe implement redux, but the general idea is here. You use it by typing whatever number you want pressing enter will create a new line and deleting the content of a line will remove it.
I hope I understood correctly what you are trying to do, here is a super scuffed version of it. You might need to change the code a bit to fit your use case.
import { useState, useEffect } from "react";
export default function App() {
const [textValue, setTextValue] = useState("");
const [displayedTextValue, setDisplayedTextValue] = useState("");
useEffect(() => {
let splitTextValue = textValue.split("\n");
splitTextValue = splitTextValue.map((line, iter) => {
if (iter === splitTextValue.length - 1) return line;
if (line[0] !== "$") return "$ " + line;
return line;
});
setDisplayedTextValue(splitTextValue.join("\n"));
}, [textValue]);
return (
<div>
<textarea
value={displayedTextValue}
onChange={(e) => {
setTextValue(e.target.value);
}}
/>
</div>
);
}
Here is a version working with key event that I think is cleaner when handling thing depending on keys.
Here is the repro on Stackblitz and here is the code :
import React from 'react';
import { render } from 'react-dom';
const App = () => {
const enterKeyCode = 'Enter';
const backspaceKeyCode = 'Backspace';
const [val, setVal] = React.useState('$ ');
const [keyCodeEvent, setKeyCodeEvent] = React.useState();
React.useEffect(() => {
if (keyCodeEvent) {
// Handle numpad 'enter' key
if (keyCodeEvent[0].includes(enterKeyCode)) {
setVal(`${val}\n$ `);
} else if (keyCodeEvent[0] === backspaceKeyCode) {
setVal(`${val.slice(0, -1)}`);
} else {
setVal(`${val}${keyCodeEvent[1]}`);
}
}
}, [keyCodeEvent]);
return (
<div>
{/* Empty onChange to prevent warning in console */}
<textarea onKeyDown={e => setKeyCodeEvent([e.code, e.key])} value={val} onChange={() => {}} />
</div>
);
};
render(<App />, document.getElementById('root'));
I read your comments on Marko Borković 's answer so I handled the backspace but he is totally right when saying you should build a special component for this. It will be way easier to improve and cleaner. You are not safe from some others bugs if you want to add features to your component.
I have a list that I can sort with drag and drop using react, and it works fine. The way it works is onDragEnter, the items get replaced. What I want to do though, is show a placeholder element once the dragging item is hovering over available space. So the final placement would happen in onDragEnd. I have two functions that handle dragging:
const handleDragStart = (index) => {
draggingItem.current = index;
};
const handleDragEnter = (index) => {
if (draggingWidget.current !== null) return;
dragOverItem.current = index;
const listCopy = [...rows];
const draggingItemContent = listCopy[draggingItem.current];
listCopy.splice(draggingItem.current, 1);
listCopy.splice(dragOverItem.current, 0, draggingItemContent);
if (draggingItem.current === currentRowIndex) {
setCurrentRowIndex(dragOverItem.current);
}
draggingItem.current = dragOverItem.current;
dragOverItem.current = null;
setRows(listCopy);
};
and in react jsx template, I have this:
{rows.map((row, index) => (
<div
key={index}
draggable
onDragStart={() => handleDragStart(index)}
onDragEnter={() => handleDragEnter(index)}
onDragOver={(e) => e.preventDefault()}
onDragEnd={handleDragEndRow}
>
...
</div>
Can anyone come with any tips as to how I might solve this?
To display a placeholder indicating where you are about to drop the dragged item, you need to compute the insertion point according to the current drag position.
So dragEnter won't do, dargOver is best suited to do that.
When dragging over the first half of the dragged overItem, the placeholder insertion point will be before the dragged over item, when dragging over the second half, it will be after. (see getBouldingClientRect, height/2 usages, of course if dragging horizontally width will need to be accounted for).
The actual insertion point (in the data, not the UI), if drag succeeds, will depend on if we're dropping before or after the initial position.
The following snippet demonstrate a way of doing that with the following changes in your initial code:
Avoided numerous refs vars by putting everything in state, especially because changing these will have an effect on the UI (will need rerender)
Avoided separate useState calls by putting all vars in a common state variable and a common setState modifier
Avoided unnecessary modifications of the rows state var, rows should change only when drag ends as it's easier to reason about it => the placeholder is not actually part of the data, it serves purpose only in the ui
Avoided defining handler in the render code onEvent={() => handler(someVar)} by using dataset key data-drag-index, the index can retrieved after using this key: const index = element.dataset.dragIndex. The handler can live with the event only which is automatically passed.
Avoided recreating (from the children props point of view) these handlers at each render by using React.useCallback.
The various css class added show the current state of each item but serves no functionnal purpose.
StateDisplay component also serves no purpose besides showing what happens to understand this answer.
Edit: Reworked and fixed fully working solution handling all tested edge cases
const App = () => {
const [state,setState] = React.useState({
rows: [
{name: 'foo'},
{name: 'bar'},
{name: 'baz'},
{name: 'kazoo'}
],
draggedIndex: -1,
overIndex: -1,
overZone: null,
placeholderIndex: -1
});
const { rows, draggedIndex, overIndex, overZone, placeholderIndex } = state;
const handleDragStart = React.useCallback((evt) => {
const index = indexFromEvent(evt);
setState(s => ({ ...s, draggedIndex: index }));
});
const handleDragOver = React.useCallback((evt) => {
var rect = evt.target.getBoundingClientRect();
var x = evt.clientX - rect.left; // x position within the element.
var y = evt.clientY - rect.top; // y position within the element.
// dataset variables are strings
const newOverIndex = indexFromEvent(evt);
const newOverZone = y <= rect.height / 2 ? 'top' : 'bottom';
const newState = { ...state, overIndex: newOverIndex, overZone: newOverZone }
let newPlaceholderIndex = placeholderIndexFromState(newOverIndex, newOverZone);
// if placeholder is just before (==draggedIndex) or just after (===draggedindex + 1) there is not need to show it because we're not moving anything
if (newPlaceholderIndex === draggedIndex || newPlaceholderIndex === draggedIndex + 1) {
newPlaceholderIndex = -1;
}
const nonFonctionalConditionOnlyForDisplay = overIndex !== newOverIndex || overZone !== newOverZone;
// only update if placeholderIndex hasChanged
if (placeholderIndex !== newPlaceholderIndex || nonFonctionalConditionOnlyForDisplay) {
newState.placeholderIndex = newPlaceholderIndex;
setState(s => ({ ...s, ...newState }));
}
});
const handleDragEnd = React.useCallback((evt) => {
const index = indexFromEvent(evt);
// we know that much: no more dragged item, no more placeholder
const updater = { draggedIndex: -1, placeholderIndex: -1,overIndex: -1, overZone: null };
if (placeholderIndex !== -1) {
// from here rows need to be updated
// copy rows
updater.rows = [...rows];
// mutate updater.rows, move item at dragged index to placeholderIndex
if (placeholderIndex > index) {
// inserting after so removing the elem first and shift insertion index by -1
updater.rows.splice(index, 1);
updater.rows.splice(placeholderIndex - 1, 0, rows[index]);
} else {
// inserting before, so do not shift
updater.rows.splice(index, 1);
updater.rows.splice(placeholderIndex, 0, rows[index]);
}
}
setState(s => ({
...s,
...updater
}));
});
const renderedRows = rows.map((row, index) => (
<div
key={row.name}
data-drag-index={index}
className={
`row ${
index === draggedIndex
? 'dragged-row'
: 'normal-row'}`
}
draggable
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
>
{row.name}
</div>
));
// there is a placeholder to show, add it to the rendered rows
if (placeholderIndex !== -1) {
renderedRows.splice(
placeholderIndex,
0,
<Placeholder />
);
}
return (
<div>
{renderedRows}
<StateDisplay state={state} />
</div>
);
};
const Placeholder = ({ index }) => (
<div
key="placeholder"
className="row placeholder-row"
></div>
);
function indexFromEvent(evt) {
try {
return parseInt(evt.target.dataset.dragIndex, 10);
} catch (err) {
return -1;
}
}
function placeholderIndexFromState(overIndex, overZone) {
if (overZone === null) {
return;
}
if (overZone === 'top') {
return overIndex;
} else {
return overIndex + 1;
}
}
const StateDisplay = ({ state }) => {
return (
<div className="state-display">
{state.rows.map(r => r.name).join()}<br />
draggedIndex: {state.draggedIndex}<br />
overIndex: {state.overIndex}<br />
overZone: {state.overZone}<br />
placeholderIndex: {state.placeholderIndex}<br />
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
.row { width: 100px; height: 30px; display: flex; align-items: center; justify-content: center; }
.row:nth-child(n+1) { margin-top: 5px; }
.row.normal-row { background: #BEBEBE; }
.row.placeholder-row { background: #BEBEFE; }
.row.normal-row:hover { background: #B0B0B0; }
.row.placeholder-row:hover { background: #B0B0F0; }
.row.dragged-row { opacity: 0.3; background: #B0B0B0; }
.row.dragged-row:hover { background: #B0B0B0; }
.state-display { position: absolute; right: 0px; top: 0px; width: 200px; }
<html><body><div id="root"></div><script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.7.0-alpha.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.7.0-alpha.0/umd/react-dom.production.min.js"></script></body></html>
I have a button that changes state when clicked. I would like a style (resultInfo) to show up on another element when the button state is false. I tried to use useEffect [checkAnswer] to update the other element's inline style, but upon the button's state change, the other element's style is not updated. How come the following doesn't work? Thanks.
// the Continue button that also shows the user if they are correct or wrong
import React, { useEffect, useContext, useState } from 'react';
import { PracticeContext } from '../contexts/PracticeContext';
import ModuleFinished from './ModuleFinished';
// Enables the user to check their answer, shows the result, and provides an element to proceed to the next question
const ModulePracticeAnswerResult = ( {questionNumber, answer, attempt} ) => {
const { setShowModuleFinished } = useContext(PracticeContext);
const { questionIndex, setQuestionIndex } = useContext(PracticeContext);
const { selectedPracticeEnd } = useContext(PracticeContext);
const [checkAnswer, setCheckAnswer] = useState(false);
// create the user selected module for practice
useEffect(() => {
setCheckAnswer(false); // answer state is reverted when question state is updated
}, [questionIndex]);
// navigate to the next question
function progress(e) {
e.preventDefault();
if (checkAnswer === false) {
setCheckAnswer(true);
return;
}
if (selectedPracticeEnd === true) {
// there are no more questions - don't progress any further
if (checkAnswer) {
setShowModuleFinished(true);
}
return;
}
// if checkAnswer is true, user has already answers and received feedback. progress to next question
setQuestionIndex(questionNumber + 1);
}
let resultInfo = { display: 'none' };
useEffect(() => {
// when check answer button has been pressed, its state changes to false until the continue button is pressed
if (checkAnswer === false) {
// display the result of the answer
if (answer === attempt) {
resultInfo = { display: 'block', background: '#4CAF50' }
}
else {
resultInfo = { display: 'block', background: 'rgb(255, 52, 86)' }
}
return;
}
resultInfo = { display: 'none' }; // user hasn't checked the answer yet
}, [checkAnswer]);
return (
<div className="module-practice-answer-result">
<div className="result-info-container">
<div style={resultInfo}>
<p>{ 'result message here...' }</p>
</div>
<button
className={ checkAnswer === false ? 'answer-button answer-button-default' : 'answer-button answer-button-continue' }
onClick={progress} disabled={attempt.length < 1 ? 'disabled' : ''}>
{ checkAnswer === false ? 'Check' : 'Continue' }
</button>
</div>
<ModuleFinished />
</div>
);
}
export default ModulePracticeAnswerResult;