Detect handle change inside componentDidUpdate method - Reactjs - reactjs

I have a form that contains several questions. Some of the questions contains a group of subquestions.
The logic to render sub questions is written inside componentDidUpdate method.
componentDidUpdate = (prevProps, prevState, snapshot) => {
if (prevProps !== this.props) {
let questions = this.props.moduleDetails.questions,
sgq = {};
Object.keys(questions).map((qId) => {
sgq[qId] = (this.state.groupedQuestions[qId]) ? this.state.groupedQuestions[qId] : [];
let answerId = this.props.formValues[qId],
questionObj = questions[qId],
groupedQuestions = [];
if(questionObj.has_grouped_questions == 1 && answerId != null && (this.state.groupedQuestions != null)) {
groupedQuestions = questions[qId].question_group[answerId];
let loopCount = this.getLoopCount(groupedQuestions);
for(let i=0; i<loopCount; i++) {
sgq[qId].push(groupedQuestions);
}
}
});
this.setState({groupedQuestions: sgq});
}
}
The problem is that on every key stroke of text field, handleChange method is invoked which will ultimately invoke componentDidUpdate method. So the same question groups gets rendered on every key stroke.
I need a way to detect if the method componentDidUpdate was invoked due to the key press(handleChange) event so that i can write logic as follows.
if(!handleChangeEvent) {
Logic to render question group
}
Any idea on how to integrate this will be appreciated.

I assume your textfield is a controlled component, meaning that its value exists in the state. If this is the case, you could compare the previous value of your textfield to the new one. If the value is different, you know the user entered something. If they are equal however, the user did something else at which point you want your snippet to actually execute.
Basically:
componentDidUpdate = (prevProps) => {
// if value of textfield didn't change:
if (prevProps.textfieldValue === this.props.textfieldValue) {
// your code here
}
}
Another approach is to use componentDidReceiveProps(). There you can compare the props to the previous ones, similarly to the above, and execute your code accordingly. Which method is most suitable depends on how your app works.

Related

How would I re-render the list everytime the state changes for this text filter using react hooks

I have updated this question with clearer and more concise code on 15/03/22.
I have a text filter and it filters an array (displaying a list) correctly if it matches something in the displayed list, but as the user deletes character by character the filter does not change. I need it to keep matching as the user delete's chars.
Here is the filter which is set as the onChange for the text field:
const [searchInputTitle, setSearchInputTitle] = useState<string>('');
const [filteredItemsArrayState, setFilteredItemsArrayState] = useState<IListItems[]>(props.requests);
const searchTitles = () => {
let filteredArrayByTitle: IListItems[] = [];
filteredArrayByTitle = theRequestsState.filter((item) => {
return item.Title && item.Title.toLowerCase().indexOf(searchInputTitle.toLowerCase()) >= 0;
});
console.log(searchInputTitle, 'searchInputTitle');
if (searchInputTitle && searchInputTitle.length > 0) {
setTheRequestsState(filteredArrayByTitle);
setIsFiltered(true);
} else if (searchInputTitle && searchInputTitle.length === 0) {
const AllItems = props.requests;
let sortedByID: IListItems[] = AllItems.sort((a, b) => a.Id > b.Id ? 1 : -1);
setTheRequestsState(sortedByID);
setIsFiltered(false);
}
};
<TextField
onChange={searchTitles}
value={searchInputTitle}
/>
useEffect(() => {
_onTitleFilterChange(null, searchInputTitle);
if (isFiltered == true) {
setFunctionalArea(null, null);
setRequestingFunction(null, null);
}
}, [isFiltered, searchInputTitle]);
////////
<DetailsList className={styles.DetailsList}
items={filteredItemsArrayState.slice((ListPage - 1) * 50, ((ListPage * 50)))}
/>
Can anyone see why the render is not updating on deletion of char and what I could use to do so?
Update: As I type a character into the search I can see it's finding the searched for char/word and also if I delete chars now it searches and finds actively, but as soon as I stop typing it reverts back to the original array of items.
Can you try to filter the array you give to the DetailsList so you never lost the data ..?
filteredItemsArrayState.filter(s => {
if (searchInputTitle.length === 0)
return true
else
return s.Title.toLowerCase().match(searchInputTitle.toLowerCase())
}).map(....)
Found the reason. Thanks for all your help.
The issue was the setIsFiltered(true); in the title filter function. Other filters were running based on this line and these other filters were recalling the unfiltered list everytime a key was pressed. I removed this line and the issue was fixed.
I have come to realise that useEffect is almost completely mandatory on most of my projects and is React hooks are quite a departure from the original React syntax.

How to correctly modify the state from the parent component before passing it down to child component

I have three components:
1st component
--2nd component
----3rd component
I need to pass a state and its handleState down from the first component to the second one. In the second one I need to filter through the state array values, I also need to update the db and the state so I make multiple API and handleState calls in a for loop inside of useEffect(). The third one renders the state data.
Here's the second component:
export default function LearnNewApp({ deck, handleShowAppChange, handleDecksChange }) {
let words = deck.words.filter(x => x.wordGroup == "newLearning")
useEffect(() => {
let unsubscribed = false;
if (words.length < 10) {
let vacant = 10 - words.length
let newDeck = JSON.parse(JSON.stringify(deck))
let unseenWords = newDeck.words.filter(x => x.wordGroup === "newUnseen")
for (let i = 0; i < vacant && i < unseenWords.length; i++) {
let wordUnseenToLearning = unseenWords[i]
wordUnseenToLearning.wordGroup = "newLearning"
callAPI(wordUnseenToLearning.id, "newLearning")
if (!unsubscribed) handleDecksChange(wordUnseenToLearning)
}
}
return () => unsubscribed = true;
}, [])
function memorized(word) {
callAPI(word.id, "first")
let wordLearningToFirst = {
...word, wordGroup: "first"
}
handleDecksChange(wordLearningToFirst)
}
function showAgain() {
}
if (words != []) return (
<CardPage words={words} handleShowAppChange={handleShowAppChange} leftButtonFunc={memorized} rightButtonFunc={showAgain} />
)
}
The state array that's being modified is an array of decks with words. So I'm trying to narrow down the chosen deck with .filter to up to ten words so they could be shown to the user in the third component. To do that I check how many words there are with showNew attribute. If there's less than ten of them I check if there are any new words in the deck to change from neverShown to showNew.
The code causes an error probably because it takes some time to do everything in useEffect(), not to mention it runs after render.
The third component has a button that triggers a function from the second component that also updates the db and state but not in a loop.
So the main problem is that I don't know how to properly fix the deck modification and subsequent render in the second component.

React hook rendering an extra time

My code is causing an unexpected amount of re-renders.
function App() {
const [isOn, setIsOn] = useState(false)
const [timer, setTimer] = useState(0)
console.log('re-rendered', timer)
useEffect(() => {
let interval
if (isOn) {
interval = setInterval(() => setTimer(timer + 1), 1000)
}
return () => clearInterval(interval)
}, [isOn])
return (
<div>
{timer}
{!isOn && (
<button type="button" onClick={() => setIsOn(true)}>
Start
</button>
)}
{isOn && (
<button type="button" onClick={() => setIsOn(false)}>
Stop
</button>
)}
</div>
);
}
Note the console.log on line 4. What I expected is the following to be logged out:
re-rendered 0
re-rendered 0
re-rendered 1
The first log is for the initial render. The second log is for the re-render when the "isOn" state changes via the button click. The third log is when setInterval calls setTimer so it's re-rendered again. Here is what I actually get:
re-rendered 0
re-rendered 0
re-rendered 1
re-rendered 1
I can't figure out why there is a fourth log. Here's a link to a REPL of it:
https://codesandbox.io/s/kx393n58r7
***Just to clarify, I know the solution is to use setTimer(timer => timer + 1), but I would like to know why the code above causes a fourth render.
The function with the bulk of what happens when you call the setter returned by useState is dispatchAction within ReactFiberHooks.js (currently starting at line 1009).
The block of code that checks to see if the state has changed (and potentially skips the re-render if it has not changed) is currently surrounded by the following condition:
if (
fiber.expirationTime === NoWork &&
(alternate === null || alternate.expirationTime === NoWork)
) {
My assumption on seeing this was that this condition evaluated to false after the second setTimer call. To verify this, I copied the development CDN React files and added some console logs to the dispatchAction function:
function dispatchAction(fiber, queue, action) {
!(numberOfReRenders < RE_RENDER_LIMIT) ? invariant(false, 'Too many re-renders. React limits the number of renders to prevent an infinite loop.') : void 0;
{
!(arguments.length <= 3) ? warning$1(false, "State updates from the useState() and useReducer() Hooks don't support the " + 'second callback argument. To execute a side effect after ' + 'rendering, declare it in the component body with useEffect().') : void 0;
}
console.log("dispatchAction1");
var alternate = fiber.alternate;
if (fiber === currentlyRenderingFiber$1 || alternate !== null && alternate === currentlyRenderingFiber$1) {
// This is a render phase update. Stash it in a lazily-created map of
// queue -> linked list of updates. After this render pass, we'll restart
// and apply the stashed updates on top of the work-in-progress hook.
didScheduleRenderPhaseUpdate = true;
var update = {
expirationTime: renderExpirationTime,
action: action,
eagerReducer: null,
eagerState: null,
next: null
};
if (renderPhaseUpdates === null) {
renderPhaseUpdates = new Map();
}
var firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
if (firstRenderPhaseUpdate === undefined) {
renderPhaseUpdates.set(queue, update);
} else {
// Append the update to the end of the list.
var lastRenderPhaseUpdate = firstRenderPhaseUpdate;
while (lastRenderPhaseUpdate.next !== null) {
lastRenderPhaseUpdate = lastRenderPhaseUpdate.next;
}
lastRenderPhaseUpdate.next = update;
}
} else {
flushPassiveEffects();
console.log("dispatchAction2");
var currentTime = requestCurrentTime();
var _expirationTime = computeExpirationForFiber(currentTime, fiber);
var _update2 = {
expirationTime: _expirationTime,
action: action,
eagerReducer: null,
eagerState: null,
next: null
};
// Append the update to the end of the list.
var _last = queue.last;
if (_last === null) {
// This is the first update. Create a circular list.
_update2.next = _update2;
} else {
var first = _last.next;
if (first !== null) {
// Still circular.
_update2.next = first;
}
_last.next = _update2;
}
queue.last = _update2;
console.log("expiration: " + fiber.expirationTime);
if (alternate) {
console.log("alternate expiration: " + alternate.expirationTime);
}
if (fiber.expirationTime === NoWork && (alternate === null || alternate.expirationTime === NoWork)) {
console.log("dispatchAction3");
// The queue is currently empty, which means we can eagerly compute the
// next state before entering the render phase. If the new state is the
// same as the current state, we may be able to bail out entirely.
var _eagerReducer = queue.eagerReducer;
if (_eagerReducer !== null) {
var prevDispatcher = void 0;
{
prevDispatcher = ReactCurrentDispatcher$1.current;
ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
}
try {
var currentState = queue.eagerState;
var _eagerState = _eagerReducer(currentState, action);
// Stash the eagerly computed state, and the reducer used to compute
// it, on the update object. If the reducer hasn't changed by the
// time we enter the render phase, then the eager state can be used
// without calling the reducer again.
_update2.eagerReducer = _eagerReducer;
_update2.eagerState = _eagerState;
if (is(_eagerState, currentState)) {
// Fast path. We can bail out without scheduling React to re-render.
// It's still possible that we'll need to rebase this update later,
// if the component re-renders for a different reason and by that
// time the reducer has changed.
return;
}
} catch (error) {
// Suppress the error. It will throw again in the render phase.
} finally {
{
ReactCurrentDispatcher$1.current = prevDispatcher;
}
}
}
}
{
if (shouldWarnForUnbatchedSetState === true) {
warnIfNotCurrentlyBatchingInDev(fiber);
}
}
scheduleWork(fiber, _expirationTime);
}
}
and here's the console output with some additional comments for clarity:
re-rendered 0 // initial render
dispatchAction1 // setIsOn
dispatchAction2
expiration: 0
dispatchAction3
re-rendered 0
dispatchAction1 // first call to setTimer
dispatchAction2
expiration: 1073741823
alternate expiration: 0
re-rendered 1
dispatchAction1 // second call to setTimer
dispatchAction2
expiration: 0
alternate expiration: 1073741823
re-rendered 1
dispatchAction1 // third and subsequent calls to setTimer all look like this
dispatchAction2
expiration: 0
alternate expiration: 0
dispatchAction3
NoWork has a value of zero. You can see that the first log of fiber.expirationTime after setTimer has a non-zero value. In the logs from the second setTimer call, that fiber.expirationTime has been moved to alternate.expirationTime still preventing the state comparison so re-render will be unconditional. After that, both the fiber and alternate expiration times are 0 (NoWork) and then it does the state comparison and avoids a re-render.
This description of the React Fiber Architecture is a good starting point for trying to understand the purpose of expirationTime.
The most relevant portions of the source code for understanding it are:
ReactFiberExpirationTime.js
ReactFiberScheduler.js
I believe the expiration times are mainly relevant for concurrent mode which is not yet enabled by default. The expiration time indicates the point in time after which React will force a commit of the work at the earliest opportunity. Prior to that point in time, React may choose to batch updates. Some updates (such as from user interactions) have a very short (high priority) expiration, and other updates (such as from async code after a fetch completes) have a longer (low priority) expiration. The updates triggered by setTimer from within the setInterval callback would fall in the low priority category and could potentially be batched (if concurrent mode were enabled). Since there is the possibility of that work having been batched or potentially discarded, React queues a re-render unconditionally (even when the state is unchanged since the previous update) if the previous update had an expirationTime.
You can see my answer here to learn a bit more about finding your way through the React code to get to this dispatchAction function.
For others who want to do some digging of their own, here's a CodeSandbox with my modified version of React:
The react files are modified copies of these files:
https://unpkg.com/react#16/umd/react.development.js
https://unpkg.com/react-dom#16/umd/react-dom.development.js

Why is my array not updated in the state in React? [duplicate]

This question already has answers here:
Why is setState in reactjs Async instead of Sync?
(8 answers)
Closed 4 years ago.
After calling this.setState the talents property still holds the old value:
onTalentModalCheckboxChange(e) {
debugger;
var talent = JSON.parse(e.target.dataset.talent);
talent.checked = !talent.checked;
if (this.maximumSelectedTalentsReached() && talent.checked) return;
const talentIndex = this.state.talents.findIndex(t => t.Id == talent.Id);
let updatedTalents = [...this.state.talents];
updatedTalents[talentIndex] = talent;
this.setState({ talents: updatedTalents });
// this method (setSearchResultsTotal) uses this.state.talents,
// but the talent that is updated here, is not updated.
// It still has the 'old' checked value
this.setSearchResultsTotal();
}
The talents property contains a list of Talent objects, which all have the checked property. My problem is that when setting the updated talent object
What am I missing here?
setState is asynchronous meaning the change doesn't happen immediately. So any function that uses state, needs to be called after the setState call completes. There are two ways to do this: use componentDidUpdate to fire off the function if this.state.talents is different than prevState.talents. Or, use the setState callback to call the function:
onTalentModalCheckboxChange(e) {
debugger;
var talent = JSON.parse(e.target.dataset.talent);
talent.checked = !talent.checked;
if (this.maximumSelectedTalentsReached() && talent.checked) return;
const talentIndex = this.state.talents.findIndex(t => t.Id == talent.Id);
let updatedTalents = [...this.state.talents];
updatedTalents[talentIndex] = talent;
// Only call this.setSearchResultsTotal after state has finished updating
this.setState({ talents: updatedTalents }, () => this.setSearchResultsTotal(); );
}
this.setState({ talents: updatedTalents }, () => {
this.setSearchResultsTotal();
});
try calling ur method like this , it will work

React change state in array (for loop)

I have a State with flights, and I have a slider to change the max price to change visibility of the flight elements.
maxpriceFilter() {
var flightOffer = this.state.flightOffer;
var sliderPrice = this.state.sliderPrice;
for (var i = 0; i < flightOffer.length; i++) {
if( flightOffer[i].price > sliderPrice) {
this.setState(
{[flightOffer[i].hiddenprice] : true}
);
};
}
This code is adding a undefined field with status true in the root of the state though.. I cant find any best practice on this, other then using computed fields. But I cant get the computed field working either..
Could someone please help me out here?
You don't want to do a setState call in a loop, that will have the react component render multiple times. Build a new state object and call the setState once. You also don't want to filter it out by an if statement, but set previous values to hidden:
maxpriceFilter() {
var flightOffer = this.state.flightOffer;
var sliderPrice = this.state.sliderPrice;
var newState = {};
for (var i = 0; i < flightOffer.length; i++) {
newState[flightOffer[i].hiddenprice] = flightOffer[i].price > sliderPrice;
}
this.setState(newState);
// ...
}
If you're still having issues, it could be that the hiddenprice property isn't what you expect? Might need to post your render() function as well.
Instead of doing your looping when you're updating the state, why not just do the switch on render?
You're probably already looping over all flightOffers in render(). So, just add the check there. Inside your render, pass hiddenPrice={offer.price > sliderPrice} as a prop, or use it directly where you control the visibility.
Why? Because the visibility of a specific item in this case is not state. It is a result of the state.

Resources