Search code examples
javascriptreactjsreact-hooks

React: return object from hook causing object re-create on every render


I try to create custom hook to validate my inputs. But if custom hook returns an object, it re-creates on every change on the page

const useInput = (initialValue) => {
    const [value, setValue] = useState(initialValue)
    const [state, setState] = useState('')

    return {value, state}
}

const user = useInput('')

That way I can get values using

user.value
user.state

But if there is multiple inputs on the page, one input triggers all other inputs re-create

useEffect(() => {
    console.log('This triggers on every change on the page')
}, [user])

But if I destruct it first like this, than everything is fine

const {userValue, userState} = useInput('')

useEffect(() => {
        console.log('Does not trigger on other inputs')
}, [userValue, userState])

Or if i return the object using useMemo, it works fine too:

const useInput = (initialValue) => {
    const [value, setValue] = useState(initialValue)
    const [state, setState] = useState('')

    const obj = useMemo(() => {
        return {value, state}
    }, [value, state])

    return obj
}

const user = useInput('')

What's the reason of this, why 'user' object changes even if there was no changes in it and it using useState on values. Why it stops re-creating after destructing or using useMemo?

I'm new to React and trying to understand it.


Solution

  • Every time you call useInput you're creating a new object with the object literal syntax {} and then returning it and storing that in user. Because it's a new object, it's a new reference in memory, so it's not considered equal to the last user object (as objects are compared by reference). As a result, your useEffect() callback triggers. However, when you use useMemo(), it will return the same object reference (for the majority of cases) if value or state haven't changed, so a new object isn't created if value and state are still the same, and instead the old object is reused.

    The destructuring works because when you destructure your values from your object, you're pulling out the primitive string values into variables. As strings are primitives, they are compared by value, unlike objects which are compared by their reference. As the values of your destructured strings in your dependency array are equal to those from the last render, your useEffect() won't trigger the callback.

    See code snippet below for further details:

    const foo = () => { // similar to your `useInput()` function
      // These behave like your state values
      const a = "a";
      const b = "b";
      
      return {a, b};
    }
    
    // --- Let's try and use objects ---
    // First render
    const obj1 = foo(); // on each render, you call `userInput()` 
    
    // Second render
    const obj2 = foo(); // `userInput()` called again, this creates a complelty new object in memory that is different to `obj1`
    console.log("Is obj1 equal to obj2?", obj1 === obj2); // false - React compares the prev object reference to the current to decide if it should call the useEffect() callback, since they're not equal, react calls the callback
    
    // --- Let's try and pull out the values ---
    // First render
    const {a, b} = foo(); // `a` and `b` are string values (primitives)
    
    // Second render (using aliasing with destructuring to avoid naming conflicts)
    const {a:a2, b:b2} = foo();
    console.log("Is a and b from the first render equal to a2 and b2 from the second?:", a === a2 && b === b2); // true - Since a2 is equal to a from the first "render", and b is equal to b2, React doesn't call the callback