Cant change variable with callback function but console logging works - javascript

I am trying to change a variable in react with a callback function but cannot seem to do so. Here is my react component:
const MyComponent = () => {
let scenePinned;
const sceneCallback = event => {
if (event && event.state === 'DURING') {
console.log('Pinned');
scenePinned = true;
} else {
console.log('Not Pinned');
scenePinned = false;
}
};
console.log(scenePinned);
return (
<div>
<div style={scenePinned ? 'pinned' : 'not pinned'}/>
{(progress, event) => (
//Stuff Happens Here
), sceneCallback(event) )}
</div>
);
}
I am using react-scrollmagic and am trying to get the scenePinned variable to change from false to true and back to false again when scene is pinned to top. The console logging of Pinned and Not Pinned is happening correctly but I cannot seem to change the scenePinned variable. I am sure this is something very basic that I am not getting but I cannot understand why this is happening. Any help would be appreciated.
Note: I have tried using state to store the value but the callback is fired on scroll so the maximum depth is exceeded when trying to use state to store the scrolling status.

You need to use state for this. Otherwise the variable is reinitialized every time the component is rendered, and the value is lost.

console.log(scenePinned);
will run for the first time when the page loads
with react we use state the handle dynamic values.
or use rxjs
or create your own object and set listeners on it. with some custom event
so ex. with state
state={scenePinned:null}
then inside render method console.log(this.state.scenePinned)

A possible solution is to define a state variable in a parent component that will pass it to <MyComponent> as a prop.
Them move the sceneCallback function to the parent component and pass it as a prop to <MyComponent>
An explanation on how to define such a callback exists in many places. Here is one: (mine... ;) https://stackoverflow.com/a/55555578/5532513

Related

React: infinite loop even though useEffect should detect change and prevent loop

const [label, setLabel] = useState([<>, <>]);
const [labelType, setLabelType] = useState('none');
useEffect( () => {
if (props.label === 'label1') {
setLabel([<div>...</div>, <div>...</div>])
}
if (props.label=== 'label2') {
setLabel([<div>...</div>, <div>...</div>]);
}
if (props.label === 'label3') {
setLabel([<div>...</div>, <div>...</div>])
}
}, [labelType] );
setLabelType(props.label)
return (
<div className=''>
<div id=''>{label}</div>
</div>
)
So I'm basically passing in a prop and just trying to use that value to decide what to set my {label} to and the initial value of props.label is 'label1'. If my console logs, I see that 'none' is changed to 'label1' by setLabelType(props.label) and my understanding is that since setLabelType() was used, the stated is updated/ component re-rendered and plus labelType specifically is changed then useEffect() is triggered and the appropriate branch/setLabel() is called based on props. Next render useEffect() is not called since 'label1' -> 'label1' is no change (vs. first time 'none' -> 'label1').
But reality is that I have an infinite loop instead, could anyone explain to me why? I think maybe I need to have setLabelType(props.label) in a function but the trigger for this code to execute is in a different component so there's no 'onChange' prop that would call it in this component (basically when sibling component is clicked, passes data to common parent, parent passes data back down to this child and use the prop to decide render). This seems really simple but I'm confused.
Thanks

need to pass an effect via props or force component reload from outside the root component in preact/react

I have a situation that an item outside the component might influence the item in the backend. ex: change the value of one of the properties that are persisted, let's say the item status moves from Pending to Completed.
I know when it happens but since it is outside of a component I need to tell to the component that it is out of sync and re-fetch the data. But from outside. I know you can pass props calling the render method again. But the problem is I have a reducer and the state will pick up the last state and if I use an prop to trigger an effect I get into a loop.
Here is what I did:
useEffect(() => {
if (props.effect && !state.effect) { //this runs when the prop changes
return dispatch({ type: props.effect, });
}
if (state.effect) { // but then I get here and then back up and so on
return ModelEffect[state.effect](state?.data?.item)}, [state.status, state.effect, props.effect,]);
In short since I can't get rid of the prop the I get the first one then the second and so on in an infinite loop.
I render the root component passing the id:
render(html`<${Panel} id=${id}/>`,
document.getElementById('Panel'));
Then the idea was that I could do this to sync it:
render(html`<${Panel} id=${id} effect="RELOAD"/>`,
document.getElementById('Panel'));
any better ways to solve this?
Thanks a lot
I resolved it by passing the initialized dispatch function to a global.
function Panel (props) {
//...
const [state, dispatch,] = useReducer(RequestReducer, initialData);
//...
useEffect(() => {
//...
window.GlobalDispatch = dispatch;
//...
}, [state.status, state.effect,]);
with that I can do:
window.GlobalDispatch({type:'RELOAD'});

React js calling setState twice when invoked from componentDidMount

Got a weird unexpected behavior in react.
I created a parent component with a method to register (push to a state-array) child elements, which the child element will invoke on componentDidMount. for some reason each element is registered twice because each call invokes setState twice on the parent component.
Notes:
I have disabled the parent-component rendering by overriding shouldComponentUpdate, and this scenario still happens.
I have checked that none of the registered elements get unmounted.
I want to avoid checks for multiple registrations, and to control it with the react life-cycle methods.
The registration method:
register_field = (field_to_register) => {
this.setState((prev_state) => {
let new_fields = prev_state.fields;
new_fields.push(field_to_register);
return {fields: new_fields};
})
}
I tried to debug it using the console debugger, and the second registration iteration happens when App.js invokes render (the first registration iteration happens before).
Current W.A is to save a property on each child element which will define if the element was registered or not.
I wanted to avoid this kind of solution, but could not find anything usefull.
register_field = (field_to_register) => {
this.setState((prev_state) => {
if(field_to_register.registered){
return;
}
field_to_register.registered = true;
let new_fields = prev_state.fields;
new_fields.push(field_to_register);
return {fields: new_fields};
})
}
Remove or comment ALL the console.log after your render function in your components.
console.log cause to re-render.

Call function only after multiple states have completed updating

Logic:
I have a dialog for converting units. It has two stages of choice for the user: units to convert from and units to convert to. I keep this stage as a state, dialogStage, for maintainability as I'm likely going to need to reference what stage the dialog is in for more features in the future. Right now it's being used to determine what action to take based on what unit is clicked.
I also have a state, dialogUnits, that causes the component to rerender when it's updated. It's an array of JSX elements and it's updated via either foundUnitsArray or convertToUnitsArray, depending on what stage the dialog is at. Currently both states, dialogStage and dialogUnits, are updated at the same moment the problem occurs.
Problem:
When choosing the convertTo units, displayConversionTo() was still being called, as though dialogStage was still set to 'initial' rather than 'concertTo'. Some debugging led to confusion as to why the if (dialogStage == 'initial') was true when I'd set the state to 'convertTo'.
I believe that my problem was that the dialogStage state wasn't updated in time when handleUnitClick() was called as it's asynchronous. So I set up a new useEffect that's only called when dialogStage is updated.
The problem now is that the dialog shows no 'convertTo' units after the initial selection. I believe it's now because dialogUnits hasn't updated in time? I've swapped my original problem from one state not being ready to another state not being ready.
Question
How do I wait until both states are updated before continuing to call a function here (e.g. handleUnitClick()?).
Or have I mistaken what the problem is?
I'm new to react and, so far, I'm only familiar with the practice of state updates automatically rerendering a component when ready, unless overridden. Updating dialogUnits was displaying new units in the dialog until I tried to update it only when dialogStage was ready. It feels like an either/or situation right now (in terms of waiting for states to be updated) and it's quite possible I've overlooked something more obvious, as it doesn't seem to fit to be listening for state updates when so much of ReactJs is built around that already being catered for with rerenders, etc.
Component code:
function DialogConvert(props) {
const units = props.pageUnits;
const [dialogUnits, setDialogUnits] = useState([]);
const [dialogStage, setDialogStage] = useState('initial');
let foundUnitsArray = [];
let convertToUnitsArray = [];
units.unitsFound.forEach(element => {
foundUnitsArray.push(<DialogGroupChoice homogName={element} pcbOnClick={handleUnitClick} />);
});
useEffect(() => {
setDialogUnits(foundUnitsArray);
}, []);
useEffect(() => {
if (dialogStage == "convertTo") {
setDialogUnits(convertToUnitsArray);
}
}, [dialogStage]);
function handleClickClose(event) {
setDialogStage('initial');
props.callbackFunction("none");
}
function handleUnitClick(homogName) {
if (dialogStage == "initial") {
// getConversionChoices is an external function that returns an array. This returns fine and as expected
const choices = getConversionChoices(homogName);
displayConversionTo(choices);
} else if (dialogStage == "convertTo") {
// Can't get this far
// Will call a function not displayed here once it works
}
}
function displayConversionTo(choices) {
let canConvertTo = choices[0]["canconvertto"];
if (canConvertTo.length > 0) {
canConvertTo.forEach(element => {
convertToUnitsArray.push(<DialogGroupChoice homogName={element} pcbOnClick={handleUnitClick} />);
});
setDialogStage('convertTo');
}
}
return (
<React.Fragment>
<div className="dialog dialog__convertunits" style={divStyle}>
<h2 className="dialogheader">Convert Which unit?</h2>
<div className='js-dialogspace-convertunits'>
<ul className="list list__convertunits">
{dialogUnits}
</ul>
</div>
<button className='button button__under js-close-dialog' onClick={handleClickClose}>Close</button>
</div>
</React.Fragment>
)
}
So, there are some issues with your implementations:
Using non-state variables to update the state in your useEffect:
Explanation:
In displayConversionTo when you run the loop to push elements in convertToUnitsArray, and then set the state dialogStage to convertTo, you should be facing the issue that the updated values are not being rendered, as the change in state triggers a re-render and the convertToUnitsArray is reset to an empty array because of the line:
let convertToUnitsArray = [];
thus when your useEffect runs that is supposed to update the
dialogUnits to convertToUnitsArray, it should actually set the dialogueUnits to an empty array, thus in any case the updated units should not be visible on click of the initial units list.
useEffect(() => {
if (dialogStage == "convertTo") {
// as your convertToUnitsArray is an empty array
// your dialogue units should be set to an empty array.
setDialogUnits(convertToUnitsArray)
}
}, [dalogStage]);
You are trying to store an array of react components in the state which is not advisable:
http://web.archive.org/web/20150419023006/http://facebook.github.io/react/docs/interactivity-and-dynamic-uis.html#what-components-should-have-state
Also, refer https://stackoverflow.com/a/53976730/10844020
Solution: What you can do is try to save your data in a state, and then render the components using that state,
I have created a code sandbox example how this should look for your application.
I have also made some changes for this example to work correctly.
In your code , since you are passing units as props from parent, can you also pass the foundUnitsArray calculated from parent itself.
setDialogUnits(props.foundUnitsArray);
and remove the below operation,
units.unitsFound.forEach(element => {
foundUnitsArray.push(<DialogGroupChoice homogName={element} pcbOnClick={handleUnitClick} />);
});

React - Cannot read property 'target' of undefined

I'm invoking a function like below and understand this is why im getting this error. Is there a way to not invoke the function but still pass the event property?
onMouseOver={(event) => { this.moveBall(event) }}
The reason for wanting to do this is so I can do a check in the function like so:
const element = event.target ? event.target : event;
As I want to re-use this function to pass an element through on load:
// Below line is in a constructor.
this.navItem = document.querySelector('.navigation__item');
// Being called after my render
this.moveBall(this.props.navItem);
Feels like this should be doable..
I've managed to fix this with the below code but I believe that there must be a better way to achieve this:
window.addEventListener('load', () => {
const activeState = document.querySelector('.navigation__item .active')
this.moveBall(activeState)
});
** Update **
Full component code
https://jsfiddle.net/fvn1pu5r/
According to your last update all you need is just move first call to this.moveBall to react lifecycle hook componentDidMount. This ensures that DOM will have .navigation_item nodes in it. So, remove lines
window.addEventListener('load', () => {
const activeState = document.querySelector('.navigation__item .active')
this.moveBall(activeState)
});
from render method and add componentDidMount method to your class, like this:
componentDidMount() {
const activeState = document.querySelector('.navigation__item .active');
this.moveBall(activeState);
}
This should work.
Your moveBall function is being called with undefined as the argument at some stage. The event.target ? check then crashes with the error you gave.
The onMouseOver is likely always fine, as React supplies that.
Instead, I imagine it's the manual call you gave at the end. Is there a time when your this.props.navItem doesn't have a value?
Worth logging out that property right before calling the function to be sure it's always as you expect.

Categories