简体   繁体   中英

Creating an array in the custom hook is re rendering value at every state change

I was creating a custom hook in react native and encountered a problem with useEffect and useState, resulting in an infinite loop.

I created a little snack to exemplify what is happening:

import React, { useEffect, useState, useMemo } from 'react';
import { Text, View, StyleSheet, Button } from 'react-native';

export default function App() {
   const [testStateVariable, setTestStateVariable] = useState(false);

    const useCustomHookTest = () => {
        let testVar = ""; // Change the value "" to [] to see the problem;
        return useMemo(
            () => ({
                testVar,
            }),
            [testVar],
            );
    };
    
    var { testVar } = useCustomHookTest();
    
    useEffect(() => {
        console.log("CHANGE OF testVar!")
    }, [testVar]);

    return (
        <View style={styles.container}>
             <Button title="Change a state to true" onPress={() => {setTestStateVariable(true)}}/>
             <Button title="Change a state to false" onPress={() => {setTestStateVariable(false)}}/>
        </View>
    );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    backgroundColor: '#ecf0f1',
    padding: 8,
  },
});

Basically, when I use an array inside a custom hook, it re-renders and changes the value of the variable every time any state changes. You can see this changing the value of testVar to [] and press the two buttons to change the states, the useEffect is fired at every state change, but this doesn't happen when the value is "".

I tried using useMemo but it didn't work,

Does someone have a solution for this problem?

Thanks

The basic gist is that "" === "" is always true, while [] === [] is always false. Strings, although are like objects, are kind of treated like primitives, in that they will always be equal to themselves. Arrays on the other hand, are special Javascript objects and two declared objects are never strictly equal to each other.

Try it!

 console.log('"" === ""', "" === ""); // true console.log('[] === []', [] === []); // false

When you assign let testVar = ""; each render cycle the testVar is always equal to the last the value from the previous render and and so the useMemo hook returns the previously computed, memoized value.

const useCustomHookTest = () => {
  let testVar = "";
  return useMemo(
    () => ({
      testVar
    }),
    [testVar] // "" === "" true, dependency didn't "change".
  );
};

Contrast this now when you assign let testVar = ""; each render cycle, the dependency is now always a new array reference and thus not strictly equal and the useMemo hook recomputes its memoized value.

const useCustomHookTest = () => {
  let testVar = []; // <-- new array each render
  return useMemo(
    () => ({
      testVar
    }),
    [testVar] // [] === [] false, new memoized value
  );
};

Since the memoized value is a new object, the dependency for the useEffect hook now sees a new object, ie { testVar: true } === { testVar: false } isn't false because the testVar property changed, but because {} === {} is also false for the same reason [] === [] is. It's a new object reference.

Beyond this it is unclear what you hope to accomplish with the following code pattern:

export default function App() {
  const [testStateVariable, setTestStateVariable] = useState(false);

  const useCustomHookTest = () => {
    let testVar = "";
    return useMemo(
      () => ({
        testVar
      }),
      [testVar]
    );
  };

  var { testVar } = useCustomHookTest();

  useEffect(() => {
    console.log("CHANGE OF testVar!");
  }, [testVar]);

  return (
    <View style={styles.container}>
      <Text>{testStateVariable.toString()}</Text>
      <Button
        title="Change a state to true"
        onPress={() => {
          setTestStateVariable(true);
        }}
      />
      <Button
        title="Change a state to false"
        onPress={() => {
          setTestStateVariable(false);
        }}
      />
    </View>
  );
}

The useCustomHookTest hook is redeclared each render cycle, so anything internal is recreated each render cycle of the parent component.

If your question is basically how to use arrays as React hook dependencies then unfortunately there aren't any great solutions. The two leading solutions are either to implement a deep equality check, or to simply JSON stringify the array.

JSON.stringify Example:

const useCustomHookTest = () => {
  let testVar = [];
  return { testVar };
};

export default function App() {
  const [testStateVariable, setTestStateVariable] = useState(false);

  const { testVar } = useCustomHookTest();

  useEffect(() => {
    console.log("CHANGE OF testVar!");
  }, [JSON.stringify(testVar)]); // <-- JSON stringify dependency

  return (
    <View style={styles.container}>
      <Text>{testStateVariable.toString()}</Text>
      <Button
        title={`Change a state to ${(!testStateVariable).toString()}`}
        onPress={() => {
          setTestStateVariable((t) => !t);
        }}
      />
    </View>
  );
}

编辑 creating-an-array-in-the-custom-hook-is-re-rendering-value-at-every-state-change

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM