React state adds 1 but subtracts 2 - javascript

I am writing a function to like/unlike posts and add or subtract 1 from the likes total. Something isn't right as the addition works correctly (adds 1) but it subtracts 2 for some reason. I am not sure if it's to do with the state update delay.
const Post = (props: PostProps) => {
const {
post: {
content,
author,
postedDate,
likes,
comments,
shares,
isLiked,
isBookmarked,
},
} = props;
const [isPostBookmarked, setIsPostBookmarked] = useState(isBookmarked);
const [isPostLiked, setIsPostLiked] = useState(isLiked);
const [likesCount, setLikesCount] = useState(likes);
const handlePostLikeClick = () => {
setIsPostLiked((prevIsLikedState) => {
setLikesCount((prevLikesCountState) => {
return prevIsLikedState
? prevLikesCountState - 1
: prevLikesCountState + 1;
});
return !prevIsLikedState;
});
return (
<IconButton
icon={Heart}
label={likesCount}
filled={isPostLiked}
onClick={handlePostLikeClick}
/>
)
};

You are nesting two different state updates in same function which leads to some fuzzy issues,
Can you try changing you handlePostLikeClick function to the one mentioned below??
const handlePostLikeClick = () => {
setLikesCount((prevLikesCountState) => {
return isPostLiked ? prevLikesCountState - 1 : prevLikesCountState + 1;
});
setIsPostLiked((prevIsLikedState) => {
return !prevIsLikedState;
});
};
You can use this code sandbox for reference

The answer given by #sumanth seems quite right, but it seems something missing. In the given answer, using same onClick function handlePostLikeClick, 2 states are going to be updated. But it should only be used to update isPostLiked. Because likesCount should be updated only if any state changes detected for isPostLiked state. Therefore, it is essential to use use effect as follows.
const [isPostLiked, setIsPostLiked] = useState(false);
const [likesCount, setLikesCount] = useState(1);
useEffect(() => {
setLikesCount((current) => (isPostLiked ? current + 1 : current - 1));
}, [isPostLiked]);
const handlePostLikeClick = () => {
setIsPostLiked((prevIsLikedState) => !prevIsLikedState);
};
Explantion
Initial state for likesCount used as 1, because during initial rendering use effect will be triggered and update likesCount to 0 (since isPostLiked initial state is false). Additionally, you can see likesCount increases its count only if isPostLiked is true.
But with #sumaneth answer, likesCount increases if isPostLiked is false.
isPostLiked ? prevLikesCountState - 1 : prevLikesCountState + 1;
Therefore, it's not tracking current change of isPostLiked state, but it's using its previous state to do the update on likesCount.
Therefore, you defenitely, need to use use effect to keep track on the current change of isPostLiked state and do the update on likesCount.

Related

React: Handling focus using array of useRef

I have created a basic Todo app but having a hard time in implementing proper focus into it. Following are the requirements for focus:
Focus should be on the newly created input field when "Add" is clicked so the user can begin typing right away.
On deleting an item "Button X", focus will move to the input field in the row which replaced the deleted row, nowhere if there are no fields left, or on the new last row if the last row was deleted.
On moving an item up, focus should be placed on the newly moved field, and all associated buttons should move alongside the element. If a field is already at the top of a list, no reordering should occur, but focus should be transferred to the topmost field nonetheless.
On moving an item down, same principle should apply here (focus should be placed on the field that is moved down). If its the last field, focus it nonetheless.
Here is my implementation.
App.js:
import React, { useState, useRef } from "https://cdn.skypack.dev/react#17.0.1";
import ReactDOM from "https://cdn.skypack.dev/react-dom#17.0.1";
const App = () => {
const [myRows, setMyRows] = useState([]);
const focusInput = useRef([]);
const onAddRow = () => {
setMyRows((prevRows) => {
return [
...prevRows,
{ id: prevRows.length, text: "", up: "↑", down: "↓", delete: "X" },
];
});
focusInput.current[myRows.length - 1].focus();
};
const onMoveUp = (index) => (event) => {
const currentState = [...myRows];
if (index !== 0) {
const prevObject = currentState[index - 1];
const nextObject = currentState[index];
currentState[index - 1] = nextObject;
currentState[index] = prevObject;
setMyRows(currentState);
}
};
const onMoveDown = (index) => (event) => {
const currentState = [...myRows];
if (index !== myRows.length - 1) {
const currObject = currentState[index];
const nextObject = currentState[index + 1];
currentState[index] = nextObject;
currentState[index + 1] = currObject;
setMyRows(currentState);
}
};
const onDelete = (index) => (event) => {
const currentState = [...myRows];
currentState.splice(index, 1);
setMyRows(currentState);
};
const onTextUpdate = (id) => (event) => {
setMyRows((prevState) => {
const data = [...prevState];
data[id] = {
...data[id],
text: event.target.value,
};
return data;
});
};
return (
<div className="container">
<button onClick={onAddRow}>Add</button>
<br />
{myRows?.map((row, index) => {
return (
<div key={row.id}>
<input
ref={(el) => (focusInput.current[index] = el)}
onChange={onTextUpdate(index)}
value={row.text}
type="text"></input>
<button onClick={onMoveUp(index)}>{row.up}</button>
<button onClick={onMoveDown(index)}>{row.down}</button>
<button onClick={onDelete(index)}>{row.delete}</button>
</div>
);
})}
</div>
);
}
ReactDOM.render(<App />,
document.getElementById("root"))
In my implementation when I click Add, focus changes but in unexpected way (First click doesn't do anything and subsequent click moves the focus but with the lag, i.e. focus should be on the next item, but it is at the prior item.
Would really appreciate if someone can assist me in this. Thanks!
The strange behaviour that you observe is caused by the fact that state updates are asynchronous. When you, in onAddRow, do this:
focusInput.current[myRows.length - 1].focus()
then myRows.length - 1 is still the last index of the previous set of rows, corresponding to the penultimate row of what you're actually seeing. This explains exactly the behaviour you're describing - the new focus is always "one behind" where it should be, if all you're doing is adding rows.
Given that description, you might think you could fix this by just replacing myRows.length - 1 with myRows.length in the above statement. But it isn't so simple. Doing this will work even less well, because at the point this code runs, right when the Add button is clicked, focusInput hasn't yet been adjusted to the new length, and nor in fact has the new row even been rendered in the DOM yet. That all happens a little bit later (although appears instantaneous to the human eye), after React has realised there has been a state change and done its thing.
Given that you are manipulating the focus in a number of different ways as described in your requirements, I believe the easiest way to fix this is to make the index you want to focus its own piece of state. That makes it quite easy to manage focus in any way you want, just by calling the appropriate state-updating function.
This is implemented in the code below, which I got working by testing it out on your Codepen link. I've tried to make it a snippet here on Stack Overflow, but for some reason couldn't get it to run without errors, despite including React and enabling Babel to transform the JSX - but if you paste the below into the JS of your Codepen, I think you'll find it working to your satisfaction. (Or, if I've misinterpreted some requirements, hopefully it gets you at least a lot closer than you were.)
Rather than just leaving you to study the code yourself though, I'll explain the key parts, which are:
the introduction of that new state variable I just mentioned, which I've called focusIndex
as mentioned, the calling of setFocusIndex with an appropriate value whenever rows are added, removed or moved. (I've been trying to follow your requirements here and it seems to work well to me, but as I said, I may have misunderstood.)
the key is the useEffect which runs whenever focusIndex updates, and does the actual focusing in the DOM. Without this, of course, the focus will never be updated on calling setFocusIndex, but with it, calling that function will "always" have the desired effect.
one last subtlety is that the "always" I put above is not strictly true. The useEffect only runs when focusIndex actually changes, but when moving rows there are some situations where it is set to the same value it had before, but where you still want to move focus. I found this happening when clicking outside the inputs, then moving the first field up or the last one down - nothing happened, when we want the first/last input to be focused. This was happening because focusIndex was being set to the value it already had, so the useEffect didn't run, but we still wanted it to in order to set the focus. The solution I came up with was to add an onBlur handler to each input to ensure that the focus index is set to some "impossible" value (I chose -1, but something like null or undefined would have worked fine as well) when focus is lost - this may seem artificial but actually better represents the fact that when the focus is on no inputs, you don't want to have a "sensible" focusIndex, otherwise the React state is saying one of the inputs is focused, when none are. Note that I also used -1 for the initial state, for much the same reason - if it starts at 0 then adding the first row doesn't cause focus to change.
I hope this helps and my explanations are clear enough - if you're confused by anything, or notice anything going wrong with this implementation (I confess I have not exactly tested it to destruction), please let me know!
import React, { useState, useRef, useEffect } from "https://cdn.skypack.dev/react#17.0.1";
import ReactDOM from "https://cdn.skypack.dev/react-dom#17.0.1";
const App = () => {
const [myRows, setMyRows] = useState([]);
const focusInput = useRef([]);
const [focusIndex, setFocusIndex] = useState(-1);
const onAddRow = () => {
setMyRows((prevRows) => {
return [
...prevRows,
{ id: prevRows.length, text: "", up: "↑", down: "↓", delete: "X" },
];
});
setFocusIndex(myRows.length);
};
useEffect(() => {
console.log(`focusing index ${focusIndex} in`, focusInput.current);
focusInput.current[focusIndex]?.focus();
}, [focusIndex]);
const onMoveUp = (index) => (event) => {
const currentState = [...myRows];
if (index !== 0) {
const prevObject = currentState[index - 1];
const nextObject = currentState[index];
currentState[index - 1] = nextObject;
currentState[index] = prevObject;
setMyRows(currentState);
setFocusIndex(index - 1);
} else {
setFocusIndex(0);
}
};
const onMoveDown = (index) => (event) => {
const currentState = [...myRows];
if (index !== myRows.length - 1) {
const currObject = currentState[index];
const nextObject = currentState[index + 1];
currentState[index] = nextObject;
currentState[index + 1] = currObject;
setMyRows(currentState);
setFocusIndex(index + 1);
} else {
setFocusIndex(myRows.length - 1);
}
};
const onDelete = (index) => (event) => {
const currentState = [...myRows];
currentState.splice(index, 1);
setMyRows(currentState);
const newFocusIndex = index < currentState.length
? index
: currentState.length - 1;
setFocusIndex(newFocusIndex);
};
const onTextUpdate = (id) => (event) => {
setMyRows((prevState) => {
const data = [...prevState];
data[id] = {
...data[id],
text: event.target.value,
};
return data;
});
};
return (
<div className="container">
<button onClick={onAddRow}>Add</button>
<br />
{myRows?.map((row, index) => {
return (
<div key={row.id}>
<input
ref={(el) => (focusInput.current[index] = el)}
onChange={onTextUpdate(index)}
onBlur={() => setFocusIndex(-1)}
value={row.text}
type="text"></input>
<button onClick={onMoveUp(index)}>{row.up}</button>
<button onClick={onMoveDown(index)}>{row.down}</button>
<button onClick={onDelete(index)}>{row.delete}</button>
</div>
);
})}
</div>
);
}
ReactDOM.render(<App />,
document.getElementById("root"))

React - Make search and filter work on a given data at same time irrespective of the order of change

I am retrieving a list of data and search for a title name from a list of todos, then i have built with Switch Material UI a component to toggle the todos that are completed and not.
You can see the full demo here => https://codesandbox.io/s/so-68247206-forked-sx9kv?file=/src/App.js
Now i want to search the todos title in the list of the todos/tasks that are completed or not completed, depending on where you switch, at the moment the search goes through in all the todos.
To reproduce the case
Launch the app in codesandbox
Switch the toggle
Search for elements (It search in all the todos list and not through the completed or not completed result list )
The relevant code is the following
The search method
const handleSearch = (event) => {
let value = event.target.value.toLowerCase();
let result = [];
result = tasks.filter((data) => {
return data.title.search(value) !== -1;
});
setFilteredData(result);
};
The complete method
const getCompleted = (e, value) => {
let result = [];
let switchValue = e.target.checked;
result = tasks.filter(({ completed }) => completed === switchValue);
setFilteredData(result);
setIsCompleted(!switchValue);
setText(isCompleted ? "Yes" : "No");
};
How can i change my search method to look through the todos completed and not completed?
There's a slight modification to your handleSearch method. When you're filtering your tasks, you can check if you have toggled on the completed switch using the isCompleted state. All you need is an additional condition inside your filter method that takes care of the is completed scenario. The following updated handleSearch method works fine.
const handleSearch = (event) => {
let value = event.target.value.toLowerCase();
let result = [];
console.log(value);
let filterCompleted = false;
if (!isCompleted) filterCompleted = true;
console.log({ filterCompleted });
result = tasks.filter((data) => {
if (filterCompleted)
return data.completed && data.title.search(value) !== -1;
else return !data.completed && data.title.search(value) !== -1;
});
setFilteredData(result);
};
I use a local variable inside handleSearch to check if you have toggled on the completed button. If it's toggled on, while filtering I only filter those tasks that have the completed property set to true and vice versa. You can check that this works here https://codesandbox.io/s/so-68247206-forked-4iz7p?file=/src/App.js.
Note: I feel the isCompleted state is working on a reverse logic. When I toggle on the switch, the isCompleted is false but it should be true instead. You can try fixing this or renaming the state (if the reverse logic was intentional and you only messed up with naming the state). Anyhow this doesn't affect the solution but makes it difficult to debug and adds some confusion.
Your filter in handleSearch searches from the original tasks array which might not be what you want :
const handleSearch = (event) => {
let value = event.target.value.toLowerCase();
let result = tasks; //Assigning initial value to result
if(isCompleted){
result = result.filter(({ completed }) => completed === isCompleted)
} //only if isCompleted is true, filter the array first
result = result.filter((data) => {
return data.title.search(value) !== -1;
}); //Filter the array based on search term
setFilteredData(result);
};
Also, your getCompleted is setting the inverse value of the checkbox with (!switchValue). You can change that along with the extra param value which is not accounted for in your onChange call.
const getCompleted = (e) => {
let switchValue = e.target.checked;
let result = tasks.filter(({ completed }) => completed === switchValue);
setFilteredData(result);
setIsCompleted(switchValue);
setText(switchValue ? "Yes" : "No"); //Is text state variable really required?
};
Here I have tried to fix it : CodeSandbox
That being said, I would recommend not having multiple state variables which can be derived from each other. Your text state variable is only used for view purposes and its value completely relies on isCompleted. You could have used a conditional operator in your return function to show "Yes/No" instead of a new state variable.
You have to save both the search value and switch value in state and use a useEffect to filter the values. Check the below fork of your code
https://codesandbox.io/s/so-68247206-forked-nkuf3?file=/src/App.js

How to make a localStorage not reset when the app renders?

I have created a localStorage that adds +1 every time the user visits my app. The problem is that every time I refresh the page, the value goes back to 0.
This is my code:
localStorage.setItem('timesVisited', 0);
useEffect(() => {
if (localStorage.getItem('timesVisited') < 5) {
let timesVisited = parseInt(localStorage.getItem('timesVisited'));
localStorage.setItem('timesVisited', ++timesVisited);
}
},[])
The idea is to make the localStorage sum +1 until it reaches 5 but it keeps on 0. What I'm doing wrong?
This line will always run and resets any previous value
localStorage.setItem('timesVisited', 0);
turn it into
if(!localStorage.getItem('timesVisited')){
localStorage.setItem('timesVisited', 0)
}
Try something like this, if localstorage not set then set 0 otherwise increment
useEffect(() => {
if (!localStorage.getItem('timesVisited')) {
localStorage.setItem('timesVisited', 0);
} else if (localStorage.getItem('timesVisited') < 5) {
let timesVisited = parseInt(localStorage.getItem('timesVisited'));
localStorage.setItem('timesVisited', ++timesVisited);
}
}, [])
If the lines you provided in the component's body, then not only will it be called every time a user visits your app, but every time the component renders.
For the behaviour you're looking for, you should load the value, and set it to 0 if it doesn't exist. You should do this outside of your component, too.
Also, note that localStorage only stores strings, so you'll want to parse what you fetch from it.
const storedValue = localStorage.getItem('timesVisited')
// localStorage only stores string
const visitsCount = storedValue ? parseInt(storedValue) : 0
function myComponent = () => {
useEffect(() => {
if(visitsCount >= 5){
return
}
localStorage.setItem('timesVisited', ++visitsCount);
}, [])
}

React autocomplete in a textarea like in an IDE (e.g. VS Code, Atom)

I try to code an auto completion feature like in the gif in React.
So suggestions appear while writing text.
However all packages I could find so far work
a) only in the beginning of the input/textarea (e.g. react-autosuggest)
b) or need a trigger character (like # or #) to open (e.g. react-textarea-autocomplete)
Do I miss some React limitation? Any hints / packages?
We ended up using the fantastic editor Slate.js.
The Mentions example can easily be changed so that the suggestions are triggered by any character (not only '#'). There you go: perfect auto suggest.
I'm actually having the same problem in regards to needing a textarea implementation, but I can help with autocomplete triggering behavior.
We have an implementation of template variables that look like this {{person.name}} which get resolved into whatever the actual value is.
In regards to the autocompletion being triggered only on the first word, you can get around that with a couple modifications to the required functions.
For instance my required functions look like this. (not a completely working example, but all the important bits)
const templateVars = Object.values(TemplateVarMap);
const variables = templateVars.map((templateVar) => {
return {
name: templateVar,
};
});
//This func, onChange, and onSuggestionSelected/Highlight are the important
//parts. We essentially grab the full input string, then slice down to our
//autocomplete token and do the same for the search so it filters as you type
const getSuggestions = (value) => {
const sliceIndex = value
.trim()
.toLowerCase()
.lastIndexOf('{{'); //activate autocomplete token
const inputValue = value
.trim()
.toLowerCase()
.slice(sliceIndex + 2); //+2 to skip over the {{
const inputLength = inputValue.length;
//show every template variable option once '{{' is typed, then filter as
//they continue to type
return inputLength === 0
? variables
: variables.filter(
(variable) => variable.name.toLowerCase().slice(0, inputValue.length) === inputValue
);
};
const getSuggestionValue = (suggestion) => suggestion.name;
const renderSuggestion = (suggestion) => <div>{suggestion.name}</div>;
onSuggestionsFetchRequested = ({ value }) => {
this.setState({
suggestions: getSuggestions(value),
});
};
onSuggestionsClearRequested = () => {
this.setState({
suggestions: [],
});
};
onChange = (event, { newValue }) => {
//onChange fires on highlight / selection and tries to wipe
//the entire input to the suggested variable, so if our value
//is exactly a template variable, don't wipe it
if (templateVars.includes(newValue)) {
return;
}
this.setState({
value: newValue,
});
};
//These both need to do similar things because one is a click selection
//and the other is selection using the arrow keys + enter, we are essentially
//manually going through the input and only putting the variable into the
//string instead of replacing it outright.
onSuggestionHighlighted = ({ suggestion }) => {
if (!suggestion) {
return;
}
const { value } = this.state;
const sliceIndex = value.lastIndexOf('{{') + 2;
const currentVal = value.slice(0, sliceIndex);
const newValue = currentVal.concat(suggestion.name) + '}}';
this.setState({ value: newValue });
};
onSuggestionSelected = (event, { suggestionValue }) => {
const { value } = this.state;
const sliceIndex = value.lastIndexOf('{{') + 2;
const currentVal = value.slice(0, sliceIndex);
const newValue = currentVal.concat(suggestionValue) + '}}';
this.setState({ value: newValue });
};
const inputProps = {
value: this.state.value,
onChange: this.onChange,
};
render() {
return (
<Autosuggest
suggestions={this.state.suggestions}
onSuggestionSelected={this.onSubjectSuggestionSelected}
onSuggestionHighlighted={this.onSubjectSuggestionHighlighted}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
getSuggestionValue={getSuggestionValue}
renderSuggestion={renderSuggestion}
inputProps={inputProps}
/>
)
}
This lets me type something like This is some text with a {{ and have autocomplete pop up, upon choosing a selection it should go to This is some text with a {{person.name}}.
The only problem here is that it requires the final two characters in the input to be {{ (or whatever your token is) for the autocomplete box to come up. I'm still playing with cursor movement and slicing the string around in different ways so if I edit a template thats not at the end the box still pops up.
Hopefully this helps.
You can try react-predictive-text

Reselect. React. Which is the best way of usage the Reselect to gets the maximum perfomance?

I'm not so far ago have met with new to me package as Reselect. I read carefully the official docs and already have an experience with my firsts selectors. But only one thing that I cannot understand - which is the best code architecture of implementation selectors in the components to create the right memoized selectors?
So, I have a 2 basic part of the code (do not be scared of the so huge code amount in this post, it's just mocked, please looks only on code logic structure):
1.The First case, component holds the logic that responses for the right render() of the component inside it own (classic case). It has connected to the Reselect selectors and receives only calculates values that Reselect throws from the incoming Redux state to this component that previously has been cached in it.
// imports Reselect selectors calculated value
import { ID, additionalInfo} from './selectors'
class Row extends Component {
// component the responce for right render logic of the component based on Reselect values
_progress = (ID, additionalInfo) => {
const { statusEffects = [] } = additionalInfo
const { timePercent: timeDisabled = 0 } = statusEffects[1] || {}
const inactiveRows = rowID === 1 || rowID === 2
const isDisabled = inactiveRows ? false : statusEffects[1] && statusEffects[1].disabled
const isStatus = (statusEffects[1] && statusEffects[1].title) === 'Checkpoint'
const progressOff = inactiveRows ? 0 : parseInt(timeDisabled.toFixed())
const statusCheckpoint = isDisabled ? `${progressOff}%` : `${progressOff}%`
const statusG = isDisabled ? `${progressOff}%` : `${progressOff}%`
const status = isStatus ? statusCheckpoint : statusG
return {
isDisabled,
progressOff,
status
}
}
render() {
// calculated values inside own component method based on values from Reselect selectors
const { isDisabled, progressOff, status } = this._progress(ID, additionalInfo)
return (
//...some view logic
{isDisabled + progressOff + status}
)
}
}
2.The second case, the component has a separated logic that response for the render of the render() method in selectors file. It has migrated in selectors file inside the Reselect createSelector wrapper. Where selectors calculates and cashes all data from Redux state and component logic func and throw on component only the final output values, directly in the render method of the component.
// createSelector logic with all component render logic
export const progress = createSelector([getId, getAdditionalInfo], (ID, additionalInfo) => {
const { statusEffects = [] } = additionalInfo
const { timePercent: timeDisabled = 0 } = statusEffects[1] || {}
const inactiveRows = rowID === 1 || rowID === 2
const isDisabled = inactiveRows ? false : statusEffects[1] && statusEffects[1].disabled
const isStatus = (statusEffects[1] && statusEffects[1].title) === 'Checkpoint'
const progressOff = inactiveRows ? 0 : parseInt(timeDisabled.toFixed())
const statusCheckpoint = isDisabled ? `${progressOff}%` : `${progressOff}%`
const statusG = isDisabled ? `${progressOff}%` : `${progressOff}%`
const status = isStatus ? statusCheckpoint : statusG
return {
isDisabled,
progressOff,
status
}
})
// component that gets calculated reselect values
class Row extends Component {
render() {
// recives the whole calculated values from Reselect selectors
const { isDisabled, progressOff, status } = progress
return (
//...some view logic
{isDisabled + progressOff + status}
)
And what I want to ask - if there are some performance differences of such two ways of Reselect usage? Which one is best on your think:
Throw from Reselect on component only the calculated values from selectors that grabbed and cashed from Redux state
or
Place all component render logic inside Reselect createSelector func and throw on the component only the final calculated values?
I'll be grateful you for any help!
There is a both cases are right. Obviously, it's no unequivocal answer to this question. You should use reselect for each calculation that you can spot in the render method of a Component.
Also, you can improve Reselect memoization on you own, please see this FAQ from official page: https://github.com/reduxjs/reselect#q-the-default-memoization-function-is-no-good-can-i-use-a-different-one
I hope it will help you to understand what you want to do there.

Categories

Resources