React useState lagging by one keystroke - javascript

Okay - I'm having an issue with my react app onChange attribute lagging by one keystroke.
I am pretty sure this has to do with this code block running before the state is being updated.
if (hex.length === 3) {
let newColor = hex.join('');
let newColors = [...colors, newColor];
setColors(newColors);
setHex([]);
}
I tried moving the above block to a useEffect hook (as such) so that it would run when the value of hex changes to remedy this.
useEffect(() => {
if (hex.length === 3) {
let newColor = hex.join('');
let newColors = [...colors, newColor];
setColors(newColors);
setHex([]);
}
}, hex)
This did not work as expected, and I'm still facing the same issue. The goal is when an input is received, if the length of the total ASCII input is 3 characters or more, it will convert that text ([61, 61, 61] for example) into hex string of 6 characters, which will eventually be converted into a color hex code.
All of my code is as follows.
import TextInput from './components/TextInput';
import Swatch from './components/Swatch';
import './App.css';
function App() {
const [colors, setColors] = useState([]);
const [hex, setHex] = useState([]);
const [text, setText] = useState();
const convertToHex = (e) => {
const inputText = e.target.value;
setText(inputText);
for (let n = 0, l = inputText.length; n < l; n++) {
let newHex = Number(inputText.charCodeAt(n)).toString(16);
let newHexArr = [...hex, newHex];
setHex(newHexArr);
}
if (hex.length === 3) {
let newColor = hex.join('');
let newColors = [...colors, newColor];
setColors(newColors);
setHex([]);
}
};
return (
<div className='App'>
<h1 id='title'>Color Palette Generator</h1>
<TextInput func={convertToHex} />
<Swatch color='#55444' />
</div>
);
}
export default App;

How is the TextInput component managing its state (the input's value)?
I suspect you might have a problem there, as changing that TextInput component with a simple controlled input:
<input type="text" onChange={ handleOnChange } value={ text } />
And using your existing code as handleOnChange works as expected:
const App = () => {
const [colors, setColors] = React.useState([]);
const [hex, setHex] = React.useState([]);
const [text, setText] = React.useState('');
const handleOnChange = (e) => {
const inputText = e.target.value;
// I thought your problem was related to `text`...:
setText(inputText);
for (let n = 0, l = inputText.length; n < l; n++) {
let newHex = Number(inputText.charCodeAt(n)).toString(16);
let newHexArr = [...hex, newHex];
setHex(newHexArr);
}
if (hex.length === 3) {
// But looking at the comments it looks like it's related to `colors`, so as
// pointed out already you should be using the functional version of `setState`
// to make sure you are using the most recent value of `colors` when updating
// them here:
setColors(prevColors => [...prevColors, hex.join('')]);
setHex([]);
}
};
return (
<div className="app">
<h1 className="title">Color Palette Generator</h1>
<input className="input" type="text" onChange={ handleOnChange } value={ text } />
<pre>{ hex.join('') }</pre>
<pre>{ colors.join(', ') }</pre>
</div>
);
}
ReactDOM.render(<App />, document.querySelector('#app'));
body {
font-family: monospace;
margin: 0;
}
.app {
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
}
.title {
display: flex;
margin: 32px 0;
}
.input {
margin: 0 4px;
padding: 8px;
border: 2px solid black;
background: transparent;
border-radius: 2px;
}
.as-console-wrapper {
max-height: 45px !important;
}
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
In any case, there are a few other things that could be improved on that code, such as using a single useState, using useCallback, adding a separate <button> to push colors to the state or actually doing the RGB to HEX conversion:
const App = () => {
const [state, setState] = React.useState({
value: '',
hex: '',
colors: [],
});
const handleInputChange = React.useCallback((e) => {
const nextValue = e.target.value || '';
const nextHex = nextValue
.trim()
.replace(/[^0-9,]/g, '')
.split(',')
.map(x => `0${ parseInt(x).toString(16) }`.slice(-2))
.join('')
.toUpperCase();
setState((prevState) => ({
value: nextValue,
hex: nextHex,
colors: prevState.colors,
}));
}, []);
const handleButtonClick = React.useCallback(() => {
setState((prevState) => {
return prevState.hex.length === 3 || prevState.hex.length === 6 ? {
value: '',
hex: '',
colors: [...prevState.colors, prevState.hex],
} : prevState;
});
}, []);
return (
<div className="app">
<h1 className="title">Color Palette Generator</h1>
<input
type="text"
className="input"
value={ state.value }
onChange={ handleInputChange } />
<input
type="text"
className="input"
value={ `#${ state.hex }` }
readOnly />
<button className="button" onClick={ handleButtonClick }>
Add color
</button>
<pre className="input">{ state.colors.join('\n') }</pre>
</div>
);
}
ReactDOM.render(<App />, document.querySelector('#app'));
body {
font-family: monospace;
margin: 0;
}
.app {
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
}
.title {
display: flex;
margin: 32px 0;
}
.input,
.button {
margin: 8px 0 0;
padding: 8px;
border: 2px solid black;
background: transparent;
border-radius: 2px;
width: 50vw;
box-sizing: border-box;
}
.button {
cursor: pointer;
}
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>

Related

How can update the DOM if the state changes?

I've created the box container dynamically according to the input changes.
If I entered 1, it will create one box
If I change the input lets say 2, its create 3 box but it should create 2
import React from 'react';
import './style.css';
export default function App() {
const [value, setValue] = React.useState();
const boxRef = React.useRef();
function createBox() {
const div = document.createElement('div');
div.classList.add('mystyle');
div.style.backgroundColor = 'white';
div.addEventListener('click', () => {
let boxColor = div.style.backgroundColor;
if (boxColor === 'white') {
div.style.backgroundColor = 'red';
} else {
div.style.backgroundColor = 'white';
}
});
return div;
}
React.useEffect(() => {
for (let i = 0; i < value; i++) {
const boxElement = createBox();
boxRef.current.appendChild(boxElement);
}
}, [value]);
function handleBoxCreate({ target: { value } }) {
setValue(value);
}
return (
<div>
<h1>BOX CREATE</h1>
<input type="number" name="boxInput" onChange={handleBoxCreate} />
<div ref={boxRef} />
</div>
);
}
/* style.css */
.mystyle {
width: 30px;
height: 30px;
border: 2px solid black;
display: inline-block;
padding: 2px;
margin-right: 5px;
}
Do I need to cleanup the dom. if so how to do it?. or is there any better way to implement the same.
Please help. ty:)
You should avoid doing direct manipulations on the DOM. Instead create a "Box" react component and render it based on the amount of your value state.
import React from "react";
import "./styles.css";
const Box = () => {
const [color, setColor] = React.useState("white");
const onClick = () => {
if (color === "white") {
setColor("red");
} else {
setColor("white");
}
};
return (
<div
className="mystyle"
style={{ backgroundColor: color }}
onClick={onClick}
/>
);
};
export default function App() {
const [value, setValue] = React.useState(0);
function handleBoxCreate({ target: { value } }) {
setValue(Number(value));
}
return (
<div>
<h1>BOX CREATE</h1>
<input type="number" name="boxInput" onChange={handleBoxCreate} />
{[...Array(value)].map((e, i) => (
<Box key={i} />
))}
</div>
);
}

How can I fix the TextInput issue? in react-native?

If i write a text korean in ingreInput and use space twice, it will automatically create a dot
like this 안.
However, this happens only when i use Korean.
what should i do ? if i want to resolve this problem?
this is my code
(Upload.js)
const IngreInput = styled.TextInput`
width: 150px;
height: 50px;
border:1px solid #A9A9A9;
margin:5px;
border-radius: 50px;
color: #292929;
text-align: center;
`;
const Upload = ({}) => {
const [ingre1, onChangeIngre1, setIngre1] = useInput('');
}
return (
<IngreInput
placeholder="ingre1"
value={ingre1}
onChange={onChangeIngre1}
/>
)
(useInput.js)
export default (initValue = null) => {
const [value, setValue] = useState(initValue);
const handler = useCallback((e) => {
setValue(e.nativeEvent.text);
}, []);
return [value, handler, setValue];
};

Cancelling requestAnimationRequest in a React component using hooks doesn't work

I am working on a progress bar (Eventually..) and I want to stop the animation (calling cancelAnimationRequest) when reaching a certain value (10, 100, ..., N) and reset it to 0.
However, with my current code, it resets to 0 but keeps running indefinitely. I think I might have something wrong in this part of the code:
setCount((prevCount) => {
console.log('requestRef.current', requestRef.current, prevCount);
if (prevCount < 10) return prevCount + deltaTime * 0.001;
// Trying to cancel the animation here and reset to 0:
cancelAnimationFrame(requestRef.current);
return 0;
});
This is the whole example:
const Counter = () => {
const [count, setCount] = React.useState(0);
// Use useRef for mutable variables that we want to persist
// without triggering a re-render on their change:
const requestRef = React.useRef();
const previousTimeRef = React.useRef();
const animate = (time) => {
if (previousTimeRef.current != undefined) {
const deltaTime = time - previousTimeRef.current;
// Pass on a function to the setter of the state
// to make sure we always have the latest state:
setCount((prevCount) => {
console.log('requestRef.current', requestRef.current, prevCount);
if (prevCount < 10) return prevCount + deltaTime * 0.001;
// Trying to cancel the animation here and reset to 0:
cancelAnimationFrame(requestRef.current);
return 0;
});
}
previousTimeRef.current = time;
requestRef.current = requestAnimationFrame(animate);
}
React.useEffect(() => {
requestRef.current = requestAnimationFrame(animate);
return () => cancelAnimationFrame(requestRef.current);
}, []);
return <div>{ Math.round(count) }</div>;
}
ReactDOM.render(<Counter />, document.getElementById('app'));
html {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
body {
font-size: 60px;
font-weight: 700;
font-family: 'Roboto Mono', monospace;
color: #5D9199;
background-color: #A3E3ED;
}
.as-console-wrapper {
max-height: 66px !important;
}
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
Code pen: https://codepen.io/fr-nevin/pen/RwrLmPd
The main problem with your code is that you are trying to cancel an update that has already been executed. Instead, you can just avoid requesting that last update that you don't need. You can see the problem and a simple solution for that below:
const Counter = () => {
const [count, setCount] = React.useState(0);
const requestRef = React.useRef();
const previousTimeRef = React.useRef(0);
const animate = React.useCallback((time) => {
console.log(' RUN:', requestRef.current);
setCount((prevCount) => {
const deltaTime = time - previousTimeRef.current;
const nextCount = prevCount + deltaTime * 0.001;
// We add 1 to the limit value to make sure the last valid value is
// also displayed for one whole "frame":
if (nextCount >= 11) {
console.log(' CANCEL:', requestRef.current, '(this won\'t work as inteneded)');
// This won't work:
// cancelAnimationFrame(requestRef.current);
// Instead, let's use this Ref to avoid calling `requestAnimationFrame` again:
requestRef.current = null;
}
return nextCount >= 11 ? 0 : nextCount;
});
// If we have already reached the limit value, don't call `requestAnimationFrame` again:
if (requestRef.current !== null) {
previousTimeRef.current = time;
requestRef.current = requestAnimationFrame(animate);
console.log('- SCHEDULE:', requestRef.current);
}
}, []);
React.useEffect(() => {
requestRef.current = requestAnimationFrame(animate);
return () => cancelAnimationFrame(requestRef.current);
}, []);
// This floors the value:
// See https://stackoverflow.com/questions/7487977/using-bitwise-or-0-to-floor-a-number.
return (<div>{ count | 0 } / 10</div>);
};
ReactDOM.render(<Counter />, document.getElementById('app'));
html {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
body {
font-size: 60px;
font-weight: 700;
font-family: 'Roboto Mono', monospace;
color: #5D9199;
background-color: #A3E3ED;
}
.as-console-wrapper {
max-height: 66px !important;
}
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
In any case, you are also updating the state many more times than actually needed, which you can avoid by using refs and the timestamp (time) provided by requestAnimationFrame to keep track of the current and next/target counter values. You are still going to call the requestAnimationFrame update function the same number of times, but you will only update the state (setCount(...)) once you know the change is going to be reflected in the UI.
const Counter = ({ max = 10, rate = 0.001, location }) => {
const limit = max + 1;
const [count, setCount] = React.useState(0);
const t0Ref = React.useRef(Date.now());
const requestRef = React.useRef();
const targetValueRef = React.useRef(1);
const animate = React.useCallback(() => {
// No need to keep track of the previous time, store initial time instead. Note we can't
// use the time param provided by requestAnimationFrame to the callback, as that one won't
// be reset when the `location` changes:
const time = Date.now() - t0Ref.current;
const nextValue = time * rate;
if (nextValue >= limit) {
console.log('Reset to 0');
setCount(0);
return;
}
const targetValue = targetValueRef.current;
if (nextValue >= targetValue) {
console.log(`Update ${ targetValue - 1 } -> ${ nextValue | 0 }`);
setCount(targetValue);
targetValueRef.current = targetValue + 1;
}
requestRef.current = requestAnimationFrame(animate);
}, []);
React.useEffect(() => {
requestRef.current = requestAnimationFrame(animate);
return () => cancelAnimationFrame(requestRef.current);
}, []);
React.useEffect(() => {
// Reset counter if `location` changes, but there's no need to call `cancelAnimationFrame` .
setCount(0);
t0Ref.current = Date.now();
targetValueRef.current = 1;
}, [location]);
return (<div className="counter">{ count } / { max }</div>);
};
const App = () => {
const [fakeLocation, setFakeLocation] = React.useState('/');
const handleButtonClicked = React.useCallback(() => {
setFakeLocation(`/${ Math.random().toString(36).slice(2) }`);
}, []);
return (<div>
<span className="location">Fake Location: { fakeLocation }</span>
<Counter max={ 10 } location={ fakeLocation } />
<button className="button" onClick={ handleButtonClicked }>Update Parent</button>
</div>);
};
ReactDOM.render(<App />, document.getElementById('app'));
html {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
body {
font-family: 'Roboto Mono', monospace;
color: #5D9199;
background-color: #A3E3ED;
}
.location {
font-size: 16px;
}
.counter {
font-size: 60px;
font-weight: 700;
}
.button {
border: 2px solid #5D9199;
padding: 8px;
margin: 0;
font-family: 'Roboto Mono', monospace;
color: #5D9199;
background: transparent;
outline: none;
}
.as-console-wrapper {
max-height: 66px !important;
}
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>

Clickable ticker in React

I have literally tried for a few hours to replicate a clickable ticker, much like they have at the very top of this site: https://www.thebay.com/
I'm confused about what triggers useEffect and long story short, I can't come up with a solution that keeps the ticker moving AND also gives the option of clicking forward/backwards via arrows. Clicking the arrow should not permanently pause the ticker.
function Ticker() {
const [tickerDisplay, setTickerDisplay] = useState('Free In-store Pickup')
const [tickerIndex, setTickerIndex] = useState(0)
const [arrowClicked, setArrowClicked] = useState(false)
const notices = [
'Easy Returns within 30 Days of Purchase',
'Free Shipping on $99+ Orders',
'Free In-store Pickup',
]
const handleClick = (side) => {
setArrowClicked(true)
switch (side) {
case 'left':
setTickerIndex(
tickerIndex === 0 ? notices.length - 1 : tickerIndex - 1
)
break
case 'right':
setTickerIndex(
tickerIndex === notices.length - 1 ? 0 : tickerIndex + 1
)
break
default:
console.log('something went wrong')
break
}
}
useEffect(() => {
if (arrowClicked) {
setTickerDisplay(notices[tickerIndex])
setTickerIndex(
tickerIndex === notices.length - 1 ? 0 : tickerIndex + 1
)
setArrowClicked(false)
return
}
setTimeout(() => {
setTickerDisplay(notices[tickerIndex])
setTickerIndex(
tickerIndex === notices.length - 1 ? 0 : tickerIndex + 1
)
console.log('This will run every 6 seconds!')
}, 6000)
}, [tickerIndex, notices, tickerDisplay, arrowClicked])
return (
<IconContext.Provider value={{ className: 'ticker-icons-provider' }}>
<div className='ticker'>
<FaChevronLeft onClick={() => handleClick('left')} />
<div className='ticker_msg-wrapper'>{tickerDisplay}</div>
<FaChevronRight onClick={() => handleClick('right')} />
</div>
</IconContext.Provider>
)
}
export default Ticker
What is the best way to code this component?
This is not a work of art and probably some things could've been done better.
Hope that suits you.
const { useRef, useState, useEffect } = React;
const getItems = () => Promise.resolve(['All of our questions are now open', 'Answers extended: 72 hours after questions open', 'Post a question or get an answer', 'Free badges on 20k points'])
const Ticker = ({onPrevious, onNext, items, currentIndex}) => {
const ref = useRef(null);
const [size, setSize] = useState({
width: 0,
widthPx: '0px',
height: 0,
heightPx: '0px'
})
useEffect(() => {
if(ref && ref.current) {
const {width, height} = ref.current.getBoundingClientRect();
setSize({
width,
widthPx: `${width}px`,
height,
height: `${height}px`
})
}
}, [ref]);
const calculateStyleForItem = (index) => {
return {
width: size.width,
transform: `translateX(${0}px)`
}
}
const calculateStyleForContainer = () => {
return {
width: `${size.width * (items.length + 1)}px`,
transform: `translateX(${-currentIndex * size.width + 2 * size.width}px)`
}
}
return <div ref={ref} className="ticker">
<div style={{width: size.widthPx, height: size.heightPx}} className="ticker__foreground">
<div onClick={onPrevious} className="arrow">{'<'}</div>
<div onClick={onNext} className="arrow">{'>'}</div>
</div>
<div>
<div style={calculateStyleForContainer()} className="ticker__values">
{items.map((value, index) => <div key={index} style={calculateStyleForItem(index)}className="ticker__value">{value}</div>)}
</div>
</div>
</div>
}
const App = () => {
const [items, setItems] = useState([]);
const [currentIndex, setCurrentIndex] = useState(0);
const [clicked, setClicked] = useState(false);
useEffect(() => {
let isUnmounted = false;
getItems()
.then(items => {
if(isUnmounted) {
return
}
setItems(items);
})
return () => {
isUnmounted = true;
}
}, [])
useEffect(() => {
if(!items.length) {
return () => {
}
}
let handle = null;
const loop = () => {
if(!clicked) {
onNext(null);
}
setClicked(false);
handle = setTimeout(loop, 2000);
}
handle = setTimeout(loop, 2000);
return () => {
clearTimeout(handle);
}
}, [items, clicked])
const onPrevious = () => {
setClicked(true);
setCurrentIndex(index => (index - 1) > -1 ? index - 1 : items.length - 1)
}
const onNext = (programmatically) => {
if(programmatically) {
setClicked(programmatically);
}
setCurrentIndex(index => (index + 1) % items.length)
}
return <div>
{items.length ? <Ticker onPrevious={onPrevious} onNext={onNext} currentIndex={currentIndex} items={items}/> : 'Loading'}
</div>
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
html, body {
box-sizing: border-box;
margin: 0;
}
.ticker {
display: flex;
justify-content: center;
align-items: center;
background: black;
font-size: 1rem;
color: white;
font-weight: bold;
padding: 1rem;
overflow: hidden;
}
.ticker__foreground {
position: absolute;
z-index: 1;
display: flex;
justify-content: space-between;
align-items: center;
}
.ticker__values {
transition: all .3s ease-in;
}
.ticker__value {
text-align: center;
display: inline-block;
vertical-align: middle;
float: none;
}
.arrow {
font-size: 1.5rem;
cursor: pointer;
padding: 1rem;
}
<script src="https://unpkg.com/react/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/babel-standalone#6/babel.min.js"></script>
<div id="root"></div>

show average rating in read mode and just total or half value in write mode

I am trying to create star rating where the functionality has to be following:
In read mode, the stars are shown as per average (should support 100%
i.e 5 or 96% i.e 4.6) in write mode, the user can only rate 1, 1.5, 2,
2.5 etc not 2.6
The read mode is working as expected but is having problem with write mode.
The problem in write mode is I cannot update the rating with non-decimal value from 1 to 5 and also half value like 1.5, 2.5, 3.5 etc. On hovering how do i decide if my mouse pointer is in the full star or half of star? Can anyone look at this, please?
I have created a sandbox for showing the demo
Here it is
https://codesandbox.io/s/9l6kmnw7vw
The code is as follow
UPDATED CODE
// #flow
import React from "react";
import styled, { css } from "styled-components";
const StyledIcon = styled.i`
display: inline-block;
width: 12px;
overflow: hidden;
direction: ${props => props.direction && props.direction};
${props => props.css && css(...props.css)};
`;
const StyledRating = styled.div`
unicode-bidi: bidi-override;
font-size: 25px;
height: 25px;
width: 125px;
margin: 0 auto;
position: relative;
padding: 0;
text-shadow: 0px 1px 0 #a2a2a2;
color: grey;
`;
const TopStyledRating = styled.div`
padding: 0;
position: absolute;
z-index: 1;
display: block;
top: 0;
left: 0;
overflow: hidden;
${props => props.css && css(...props.css)};
width: ${props => props.width && props.width};
`;
const BottomStyledRating = styled.div`
padding: 0;
display: block;
z-index: 0;
`;
class Rating extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
rating: this.props.rating || null,
// eslint-disable-next-line
temp_rating: null
};
}
handleMouseover(rating) {
console.log("rating", rating);
this.setState(prev => ({
rating,
// eslint-disable-next-line
temp_rating: prev.rating
}));
}
handleMouseout() {
this.setState(prev => ({
rating: prev.temp_rating
}));
}
rate(rating) {
this.setState({
rating,
// eslint-disable-next-line
temp_rating: rating
});
}
calculateWidth = value => {
const { total } = this.props;
const { rating } = this.state;
return Math.floor((rating / total) * 100).toFixed(2) + "%";
};
render() {
const { disabled, isReadonly } = this.props;
const { rating } = this.state;
const topStars = [];
const bottomStars = [];
const writableStars = [];
console.log("rating", rating);
// eslint-disable-next-line
if (isReadonly) {
for (let i = 0; i < 5; i++) {
topStars.push(<span>★</span>);
}
for (let i = 0; i < 5; i++) {
bottomStars.push(<span>★</span>);
}
} else {
// eslint-disable-next-line
for (let i = 0; i < 10; i++) {
let klass = "star_border";
if (rating >= i && rating !== null) {
klass = "star";
}
writableStars.push(
<StyledIcon
direction={i % 2 === 0 ? "ltr" : "rtl"}
className="material-icons"
css={this.props.css}
onMouseOver={() => !disabled && this.handleMouseover(i)}
onFocus={() => !disabled && this.handleMouseover(i)}
onClick={() => !disabled && this.rate(i)}
onMouseOut={() => !disabled && this.handleMouseout()}
onBlur={() => !disabled && this.handleMouseout()}
>
{klass}
</StyledIcon>
);
}
}
return (
<React.Fragment>
{isReadonly ? (
<StyledRating>
<TopStyledRating
css={this.props.css}
width={this.calculateWidth(this.props.rating)}
>
{topStars}
</TopStyledRating>
<BottomStyledRating>{bottomStars}</BottomStyledRating>
</StyledRating>
) : (
<React.Fragment>
{rating}
{writableStars}
</React.Fragment>
)}
</React.Fragment>
);
}
}
Rating.defaultProps = {
css: "",
disabled: false
};
export default Rating;
Now the writable stars is separately done to show the stars status when hovering and clicking but when I am supplying rating as 5 it is filling the third stars instead of 5th.
I think your current problem seems to be with where your mouse event is set, as you are handling it on the individual stars, they disappear, and trigger a mouseout event, causing this constant switch in visibility.
I would rather set the detection of the rating on the outer div, and then track where the mouse is in relation to the div, and set the width of the writable stars according to that.
I tried to make a sample from scratch, that shows how you could handle the changes from the outer div. I am sure the formula I used can be simplified still, but okay, this was just to demonstrate how it can work.
const { Component } = React;
const getRating = x => (parseInt(x / 20) * 20 + (x % 20 >= 13 ? 20 : x % 20 >= 7 ? 10 : 0));
class Rating extends Component {
constructor() {
super();
this.state = {
appliedRating: '86%'
};
this.setParentElement = this.setParentElement.bind( this );
this.handleMouseOver = this.handleMouseOver.bind( this );
this.applyRating = this.applyRating.bind( this );
this.reset = this.reset.bind( this );
this.stopReset = this.stopReset.bind( this );
}
stopReset() {
clearTimeout( this.resetTimeout );
}
setParentElement(e) {
this.parentElement = e;
}
handleMouseOver(e) {
this.stopReset();
if (e.currentTarget !== this.parentElement) {
return;
}
const targetRating = getRating(e.clientX - this.parentElement.offsetLeft);
if (this.state.setRating !== targetRating) {
this.setState({
setRating: targetRating
});
}
}
applyRating(e) {
this.setState({
currentRating: this.state.setRating
});
}
reset(e) {
this.resetTimeout = setTimeout(() => this.setState( { setRating: null } ), 50 );
}
renderStars( width, ...classes ) {
return (
<div
onMouseEnter={this.stopReset}
className={ ['flex-rating', ...classes].join(' ')}
style={{width}}>
<span onMouseEnter={this.stopReset} className="star">★</span>
<span onMouseEnter={this.stopReset} className="star">★</span>
<span onMouseEnter={this.stopReset} className="star">★</span>
<span onMouseEnter={this.stopReset} className="star">★</span>
<span onMouseEnter={this.stopReset} className="star">★</span>
</div>
);
}
renderFixed() {
return this.renderStars('100%', 'fixed');
}
renderReadOnlyRating() {
const { appliedRating } = this.state;
return this.renderStars( appliedRating, 'readonly' );
}
renderWriteRating() {
let { setRating, currentRating } = this.state;
if (setRating === 0) {
setRating = '0%';
}
if (currentRating === undefined) {
currentRating = '100%';
}
return this.renderStars( setRating || currentRating, 'writable' );
}
render() {
return (
<div>
<div
ref={ this.setParentElement }
className="rating"
onMouseMove={ this.handleMouseOver }
onMouseOut={ this.reset }
onClick={ this.applyRating }>
{ this.renderFixed() }
{ this.renderReadOnlyRating() }
{ this.renderWriteRating() }
</div>
<div>Current rating: { ( ( this.state.currentRating || 0 ) / 20) }</div>
</div>
);
}
}
ReactDOM.render( <Rating />, document.getElementById('container') );
body { margin: 50px; }
.rating {
font-family: 'Courier new';
font-size: 16px;
position: relative;
display: inline-block;
width: 100px;
height: 25px;
align-items: flex-start;
justify-content: center;
align-content: center;
background-color: white;
}
.flex-rating {
position: absolute;
top: 0;
left: 0;
display: flex;
height: 100%;
overflow: hidden;
cursor: pointer;
}
.fixed {
color: black;
font-size: 1.1em;
font-weight: bold;
}
.readonly {
color: silver;
font-weight: bold;
}
.writable {
color: blue;
background-color: rgba(100, 100, 100, .5);
}
.star {
text-align: center;
width: 20px;
max-width: 20px;
min-width: 20px;
}
<script id="react" src="https://cdnjs.cloudflare.com/ajax/libs/react/15.6.2/react.js"></script>
<script id="react-dom" src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/15.6.2/react-dom.js"></script>
<div id="container"></div>

Categories