Samat Davletshin
Samat Davletshin

Reputation: 73

Function passed and invoked in child component doesn't have access to parent state value

I have a parent and a child components.

Parent component has state variable that I want to access.

I pass a function to child component and call it there onClick.

However, the value of the state variable is out of date - not the current value.

function Parent(props) {
    const [items, setItems] = useState([]);

    function getItems () {
        console.log(items); // always prints "[]" 
    }

    useEffect(() => {
        thirdPartyApiCall().then((fetchedItems) =>
            let newItems = fetchedItems.map(item => <Child id={item.id} getItems={getItems}/>)
            setItems(newItems); // populates items array. "{items}" elements correctly displayed on web page
        );
    }, []);

}


function Child(props) {
    return <button onClick={props.getItems}>{props.id}</button>
}

Upvotes: 2

Views: 1432

Answers (2)

Balavishnu V J
Balavishnu V J

Reputation: 125

You might want to look at how javascript closure works.

Will give a basic idea of what's happening.

useEffect would only run once, due to the dependency. So when the child components were rendered items was an empty array([]). Your child components would always have this instance of items as useEffect did not get called with updated items.

Hope this helped.

EDIT:1

You might want your parent component to look like this.

function Parent(props) {
    const [items, setItems] = useState([]);

    function getItems () {
        console.log(items); // always prints "[]" 
    }

    useEffect(() => {
        thirdPartyApiCall().then((fetchedItems) =>
            setItems(newItems);
        );
    }, []);

    return newItems.map(item => <Child key={item.id} id={item.id} getItems={getItems}/>)
}

Upvotes: 3

Nino Filiu
Nino Filiu

Reputation: 18493

Well, yes, of course clicking on the button logs []. In this:

<Child id={item.id} getItems={getItems}/>

getItem gets evaluated as a function that logs the items and the items are [], so [] is logged. The reason why it doesn't logs the new items is because, as your code is being written, React cannot really know when to update the component, because no variable is used in the render phase.


This is generally a bad practice to use JSX elements outside of Render because they're not really normal JS variables with predictable behavior. The fact that it compiles is more a hack than a feature.

Keep vanilla variables in the state, and let the components do the JSX rendering. This approach is slightly different than yours, but works as expected, as clicking on either button logs all the items:

const fakeApiCall = () => Promise.resolve([
    {id: 10},
    {id: 20},
    {id: 30},
]);

const Parent = () => {
    const [items, setItems] = React.useState([]);
    React.useEffect(() => {
        fakeApiCall().then(setItems)
    })
    const getItems = () => console.log(items);
    return (
        <div>
            {items.map(item => (
                <Child
                id={item.id}
                key={item.id}
                getItems={getItems}/>
            ))}
        </div>
    )
}

const Child = ({id, getItems}) => (<button onClick={getItems}>{id}</button>);

window.onload = () => ReactDOM.render(<Parent/>, document.querySelector('#root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>

<div id="root"></div>

Upvotes: 2

Related Questions