Home > Blockchain >  Function can't read useState parameters
Function can't read useState parameters

Time:04-08

I'm building an app with React Native and Firebase Realtime Database, when I start the function addItem the only parameter in the object that I get from the Database is the id, the other paramters return as '', I've tried using console.log() and the TextInputs work fine also I've used this configuration a lot of times and this is the first time it happens.

export default function NewItem() {

    const [name, setName] = useState('');
    const [price, setPrice] = useState('');
    const [description, setDescription] = useState('');

    useEffect(() => {
        navigation.setOptions({
            headerRight: () => (
                <TouchableOpacity onPress={addItem}>
                    <Text style={{fontWeight:'bold'}}>ADD ITEM</Text>
                </TouchableOpacity>
            )
        })
    }, [])

    const addItem = () => {

        const changes = ref(db, 'path')
        get(changes).then( async (snapshot) => {
            if (snapshot.val().data !== undefined) {

                let array = []

                let object = {
                    "id": `${id}`,
                    "name": name,
                    "price": price,
                    "description": description,
                }

                array.push(object)
                
                update(changes, {
                    data: array
                })

            }
        })
    }

  return (
    <TouchableWithoutFeedback>
            <TextInput
                onChangeText={(e) => setName(e)}
            />
            <TextInput
                onChangeText={(e) => setShippingPrice(e)}
                    />
            <TextInput
                onChangeText={(e) => setPrice(e)}
            />
            <TextInput
                onChangeText={(e) => setDescription(e)}
            />
    </TouchableWithoutFeedback>
  )
}

CodePudding user response:

addItem is a stale enclosure over the state it references since the useEffect hook runs only once and closes over the initial state. You can likely resolve by re-enclosing the updated state by adding addItem function to the dependency array.

useEffect(() => {
  navigation.setOptions({
    headerRight: () => (
      <TouchableOpacity onPress={addItem}>
        <Text style={{fontWeight:'bold'}}>ADD ITEM</Text>
      </TouchableOpacity>
    )
  })
}, [addItem]);

You should memoize the addItem callback handler to further reduce the number of times the useEffect hook above re-encloses the callback/state.

const addItem = React.useCallback(() => {
  const changes = ref(db, 'path');
  get(changes)
    .then(async (snapshot) => {
      if (snapshot.val().data !== undefined) {
        const data = [{
          "id": `${id}`,
          name,
          price,
          description,
        }];
                
        update(changes, { data });
      }
    })
}, [name, price, description]);

Alternatively you could cache all the state values in a React ref and access them via the ref in the callback.

Example:

export default function NewItem() {
  const [name, setName] = useState('');
  const [price, setPrice] = useState('');
  const [description, setDescription] = useState('');

  const stateRef = React.useRef({
    name,
    price,
    description,
  });

  useEffect(() => {
    stateRef.current = { name, price, description };
  }, [name, price, description]);

  useEffect(() => {
    navigation.setOptions({
      headerRight: () => (
        <TouchableOpacity onPress={addItem}>
          <Text style={{fontWeight:'bold'}}>ADD ITEM</Text>
        </TouchableOpacity>
      )
    })
  }, []);

  const addItem = () => {
    const changes = ref(db, 'path');
    get(changes)
      .then(async (snapshot) => {
        if (snapshot.val().data !== undefined) {
          const { name, price, description } = stateRef.current;
          const data = [{
            "id": `${id}`,
            name,
            price,
            description,
          }];
                
          update(changes, { data });
        }
      })
  }

  return (...);
}

CodePudding user response:

As Drew Reese pointed out, this is the typical stale state issue in React, when you use JavaScript closures. useEffect takes a function as parameter, in that function you use another function addItem that is declared inside a React Component and uses React State. What happens in this case is that addItem gets memoized in the condition it is when useEffect is executed, it's like a static picture of it. Unfortunately addItem is not a pure function and it relies on React state, so it's gonna change during React lifecycle, while useEffect callback version of it is going to remain a static picture. This happens because you initialized useEffect hook with an empty array as a second parameter, [].

That's why React hooks introduce the dependencies array, the values inside that array determine when the callback inside the hook has to be re-evaluated. There's a very important Linting rule that forces you to put all the dependencies of your callback inside the deps array: @react-hooks/exhaustive-deps to avoid a lot of subtle bugs and misbehaviours.

This should be the correct way to use React hooks in your case, to improve performance and avoid bugs and unnecessary calculations on rerenders:

export default function NewItem() {

    const [name, setName] = useState('');
    const [price, setPrice] = useState('');
    const [description, setDescription] = useState('');

    useEffect(() => {
        navigation.setOptions({
            headerRight: () => (
                <TouchableOpacity onPress={addItem}>
                    <Text style={{fontWeight:'bold'}}>ADD ITEM</Text>
                </TouchableOpacity>
            )
        })
    }, [addItem])

    const addItem = useCallback(() => {

        const changes = ref(db, 'path')
        get(changes).then( async (snapshot) => {
            if (snapshot.val().data !== undefined) {

                const array = []

                const object = {
                    "id": `${id}`,
                    "name": name,
                    "price": price,
                    "description": description,
                }

                array.push(object)
                
                update(changes, {
                    data: array
                })

            }
        })
    },[name, price, description ])

  return (
    <TouchableWithoutFeedback>
            <TextInput
                onChangeText={(e) => setName(e)}
            />
            <TextInput
                onChangeText={(e) => setShippingPrice(e)}
                    />
            <TextInput
                onChangeText={(e) => setPrice(e)}
            />
            <TextInput
                onChangeText={(e) => setDescription(e)}
            />
    </TouchableWithoutFeedback>
  )
}
  • Related