Reputation: 1601
I am trying to update the value of a state from another function.
I have declared them like so:
const App () => {
const [positionAX, setPositionAX] = useState(100)
const [positionAY, setPositionAY] = useState(100)
}
outside of App
I have another function:
const setPosition = () => {
setPositionAX(200);
setPositionAY(200);
}
When I call setPosition()
I get the error:
Cant find Variable: setPositionAX
Cant find Variable: setPositionAY
I tried to move const [positionAX, setPositionAX] = useState(100)
to the top of my file but then I got the error:
Invalid hook call. Hooks can only be called inside of the body of a function.
How do I update that variable from other functions?
I can't seem to get this to work, I am not sure what I am doing wrong
Here is a live snack example of the code you gave me: https://snack.expo.io/Cd!hwDQ0m
If I add an alert:
alert("pressed")
Into the handleClick function, that works so for some reason it is just not working with the setPositionA([200, 200])
.
Also as an additional note: In my app example, there are two functions and the functionality of setting the state, and the functionality of displaying the state are separated which is why I am getting the issue I meant to present in my initial Stack overflow post.
Here is a snack of that example of what I am trying to achieve:
https://snack.expo.io/6TUAkpCZM
I am using the 'positionX' and 'positionY' as vars to define the Top and Left position of an element which are rendered out, however, the actual setting of the positionX
and positionY
variable is done in another function
I am a junior developer so I really want to thank you for all your help with this and also apologise if this is super nooby.
I tried your way and I just can't seem to get it to work. I think there is some confusion as to what I want to achieve and where I want to achieve it. That is probably my fault as I am a noobie developer so please forgive me. I didn't want to just post all my code as I think its frowned upon for people to just code dump and be like "fix this" - however, for this situation its probably best haha.
Here is a full snack of my code: https://snack.expo.io/uk8yb5xys
What I want is this:
The user is instructed to draw a line from the word "start" to the word "end". If the line is drawn and the start point and endpoint of the users line match the given path, they are then presented with another line to draw and the position of "start" and "end" will move so the user knows where to draw.
There is a comment line:
// HERE IS WHERE I NEED TO UPDATE THE VALUES FOR START AND END
This is where I need the word "start" and the word "end" to move to their new positions.
At the moment, when you draw a complete line and remove your finger from the screen - nothing happens. However, when you put your finger down the start and end do move. I need them to move when the user lifts up their finger.
I appreciate your time with this so thank you.
Upvotes: 8
Views: 21662
Reputation: 2582
Three options:
const setPositionFactory = (setPositionAX, setPositionAY) => (pos = 200) => {
setPositionAX(pos);
setPositionAY(pos);
}
So you can call it inside your component in many ways:
Directly:setPositionFactory(setPositionAX, setPositionAY)(); // default: 200
or Keeping the function for later use:
const setPosition = setPositionFactory(setPositionAX, setPositionAY);
// ...
setPosition(220);
App
and update state inside the component. It would be more idiomatic.Observable
with rxjs and subscribe your component to it.Adding observables to your question's code would be:
import { positionObs } from './observables';
const App () => {
const [positionAX, setPositionAX] = useState(100);
const [positionAY, setPositionAY] = useState(100);
positionObs.subscribe((pos) => {
setPositionAX(pos);
setPositionAY(pos);
});
}
Then in observables
import { Subject } from 'rxjs';
export const positionObs = new Subject();
export const setPosition = () => {
positionObs.next(200);
};
Upvotes: 6
Reputation: 25423
useState
allowing React to queue up the re-rendering of that specific component
you can pass APP.js function down to AnotherComponent and call App.js function from AnotherComponent and manage state on both component and other way to use REDUX if your structure is complex
you can test Snack Demo here: https://snack.expo.io/iVeYXymID
Fix according to edited question: https://snack.expo.io/i_RJHi0_v
import React, { useState, useRef } from "react";
import { Text, View, Button, StyleSheet } from "react-native";
const AnotherComponent = (props) => {
const [positionXY, setPositionXY] = useState(props.StateValue);
const handleClick = () => {
const value = [positionXY[0]+5, positionXY[1]+5];
setPositionXY(value);
props.setPositionValues(value);
};
return (
<View style={styles.anotherComponentStyle}>
<Text>
AnotherComponent PositionX: {positionXY[0]} PositionY: {positionXY[1]}
</Text>
<Button onPress={handleClick} title="Update Position"></Button>
</View>
);
};
const Sandbox = () => {
const StateValue = [0, 0];
const [positionXY, setPositionXY] = useState(StateValue);
const setPositionValues = (value) => {
setPositionXY(value);
};
return (
<View style={styles.container}>
<AnotherComponent setPositionValues={setPositionValues} StateValue={StateValue} />
<Text
style={{ positon: "absolute", top: positionXY[0], left: positionXY[1] }}
>
Sandbox component PositionX: {positionXY[0]} PositionY: {positionXY[1]}
</Text>
</View>
);
};
export default Sandbox;
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: "center", alignContent: "center" },
anotherComponentStyle: {
backgroundColor: "#eeeffe",
borderBottomColor: "#000000",
borderBottomWidth: 2,
marginBottom: 200,
justifyContent: "center",
alignItems: "center",
},
});
Upvotes: 2
Reputation: 1
We can send the values ( in our case , 200 ) as props to the App function component. React.useEffect()
helps in listening to change in props. So, whenever we want to set the values, we can.
Parent.js
class Parent extends React.Component {
constructor(props) {
super(props)
this.state = {
x: 0,
y: 0
}
}
clickHandler = () => {
this.setState({
x : 200,
y : 200
})
}
render() {
return (
<div>
<Button onPress={this.clickHandler} title="set 200"/>
<App setX={this.state.x} setY={this.state.y} />
</div>
)
}
}
App.js
const App = (props) => {
const [positionAX, setPositionAX] = React.useState(100)
const [positionAY, setPositionAY] = React.useState(100)
React.useEffect(()=>{
setPositionAX(props.setX)
setPositionAY(props.setY)
})
return <div>{positionAX}, {positionAY}</div> //displays the position
}
Upvotes: 0
Reputation: 4749
Let me first start with why your app on expo snack doesn't work (given this https://snack.expo.io/uk8yb5xys is the snack of your original app). Your data (currentStroke and currentPath) is stored in just a variable outside of react - it is not reactive. That means while yes, your components read this data when they render (for example, Start and End objects determine their position based on it), after initial render, changing currentStroke doesn't change Start and End position because it doesn't trigger a re-render of those components. So data is changed "correctly", but components don't redraw itself with new data. If you want data changes to be reflected in the app, you should most definitely keep it in state
Here, I only made your CharacterTrace
update every half second no matter what, which is not something you should ever do, but it is here to illustrate the point: now it will redraw itself every half a second and "pick up" changes to your non-reactive currentStroke
:
const CharacterTrace = () => {
const [path, setPath] = useState(emptyPath);
//
const [_,updateState] = useState()
React.useEffect(() => {
setInterval(() => {
console.log('updating state only to trigger re-render...')
updateState(Math.random())
}, 500)
} , [])
//
...
snack with this change: https://snack.expo.io/wwJAtOjfv
So, to solve your problem you should rework your app to store currentStroke
in state, for example, in CharacterTrace
component const [currentStroke, setCurrentStroke] = useState(1)
. Then, you will pass setCurrentStroke
to underlying components to do their logic and call it when necessary
That should solve your problem the correct way, but for the sake of answering your original question on how to call setter outside of the component:
.
const A = () => {
const [x, setX] = useState(0)
return (
<B setX={setX} />
)
}
const B = ({ setX }) => {
useEffect(() => {
setX(123)
}, [])
// ...
}
if you want to change it from parent component, what you should actually do is to lift the state up to that parent component, because it belongs there
if you need to change state from outside of react components, which you will almost never have to do, for the sake of answer completeness this is how you can do it
.
let visibleSetXReference = null
const App = () => {
const [x, setX] = useState(0)
visibleSetXReference = setX
return (
<View>
<Text>{x}</Text>
</View>
)
}
const setXFromOutsideApp = (newX) => {
if (!visibleSetXReference) {
console.log("App hasn't rendered yet")
} else {
visibleSetXReference(newX)
}
}
setXFromOutsideApp(123)
setXFromOutsideApp(456)
setXFromOutsideApp(789)
Upvotes: 0
Reputation: 2585
Hooks can ONLY be used in components!
The answer is really simple: Put the method you want to use the hook inside the component.
const App () => {
const setPosition = () => {
setPositionAX(200);
setPositionAY(200);
}
const [positionAX, setPositionAX] = useState(100)
const [positionAY, setPositionAY] = useState(100)
}
You think you really have to call useState in a seperate function?
Upvotes: 0
Reputation: 846
Since logic of the function that you placed outside of the function component is pretty closely coupled with logic inside of it, I'd suggest to move it inside.
Your file would look something like this:
import React, { useState, useRef } from 'react';
import { Text, View, Button } from 'react-native';
const Sandbox = () => {
const [positionA, setPositionA] = usePosition([0, 0]);
const [x, setX] = useState(0);
const [y, setY] = useState(0);
const usePosition = ([_x = 0, _y = 0] = []) => {
const { current: setPosition } = useRef(function setPosition([_x, _y] = []) {
setX(x);
setY(y);
});
return [[x, y], setPosition];
};
const handleClick = () => {
setPositionA([200, 200]);
};
return (
<View>
<Text style={{ marginTop: 40, marginLeft: 40 }}>Position: {positionA.join()}</Text>
<Button onPress={handleClick} title="Update Position"></Button>
</View>
);
};
export default Sandbox
Upvotes: 0
Reputation: 1075755
The state setters you get from useState
are specific to the component instance, created when the component function is called the first time.
You have a few options:
You can pass setPositionAX
and setPositionAY
into setPosition
as arguments.
You can put your setPosition
function in your component function. (Yes, it'll be recreated each time, but that's fairly harmless.)
You can create your own hook for position information.
#3 looks like this:
const usePosition = (_x = 0, _y = 0) => {
const [x, setX] = useState(_x);
const [y, setY] = useState(_y);
const {current: setPosition} = useRef(function setPosition(_x = 0, _y = 0) {
setX(x);
setY(y);
});
return [x, y, setPosition];
};
Then in your component function:
const [xA, yA, setPositionA] = usePosition(0, 0);
// ...
setPositionA(200, 200);
Or if you prefer a tuple for the position information:
const usePosition = ([_x = 0, _y = 0] = []) => {
// ^^^^^^^^^^^^^^^ ^^^^^−− optional default value
// \ \
// +−−−−−−−−−−−−+−−−−−−− destructuring
const [x, setX] = useState(_x);
const [y, setY] = useState(_y);
// (Ensure the setter is consistent across calls, like React does)
const {current: setPosition} = useRef(function setPosition([_x = 0, _y = 0] = []) {
setX(x);
setY(y);
});
return [ [x, y], setPosition ];
};
Then in your component function:
const [positionA, setPositionA] = usePosition([0, 0]);
// ^−−−−^−−−−− passing in a tuple
// ...
setPositionA([200, 200]);
Here's an example of that tuple version using React:
const { useState, useRef } = React;
const usePosition = ([_x = 0, _y = 0] = []) => {
const [x, setX] = useState(_x);
const [y, setY] = useState(_y);
const {current: setPosition} = useRef(function setPosition([_x = 0, _y = 0] = []) {
setX(x);
setY(y);
});
return [ [x, y], setPosition ];
};
const Example = () => {
const [positionA, setPositionA] = usePosition([0, 0]);
const handleClick = () => {
setPositionA([200, 200]);
};
return (
<div>
<div>Position: {positionA.join()}</div>
<input type="button" onClick={handleClick} value="Set 200x200" />
</div>
);
};
ReactDOM.render(<Example/>, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.12.0/umd/react-dom.production.min.js"></script>
Upvotes: 2