JamesG
JamesG

Reputation: 1601

React Native - how to use 'useState()' in another function

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?

Edit 1 for @t-j-crowder

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.

Edit 2 for: Muhammad Numan

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

Answers (7)

Ignacio Lago
Ignacio Lago

Reputation: 2582

Three options:

A. Send those functions as params and use curried functions:

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);

B. Get those values as props of App and update state inside the component. It would be more idiomatic.

C. Use an 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

Muhammad Numan
Muhammad Numan

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",
  },
});

enter image description here

Upvotes: 2

MukeshVM14
MukeshVM14

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

Max
Max

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:

  1. if you want to change it from child components, pass setter as a prop

.

const A = () => {
  const [x, setX] = useState(0)

  return (
    <B setX={setX} />
  )
}

const B = ({ setX }) => {
  useEffect(() => {
    setX(123)
  }, [])
  // ...
}
  1. 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

  2. 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

mleister
mleister

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?

  • You probably have a design/thinking/logic-issue.
  • For variables that needs to be set outside the component (e.g. global state) you better use Redux or useContext()-hook. This would be the best-practive approach.

Upvotes: 0

Uma
Uma

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

T.J. Crowder
T.J. Crowder

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:

  1. You can pass setPositionAX and setPositionAY into setPosition as arguments.

  2. You can put your setPosition function in your component function. (Yes, it'll be recreated each time, but that's fairly harmless.)

  3. 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

Related Questions