Reputation: 4341
I have a react code that
empty state
fills
the staterenders
some imagesonLoad
eventfunction
that reads
the initial state
empty
How that can be? If the function is called, it means that the state is not empty anymore
The pen https://codesandbox.io/s/usestate-strange-things-9rydu
The code
import React, { useRef, useState, useEffect } from "react";
import styled from "@emotion/styled";
const useMyHook = (virtual_structure, setVirtual_structure) => {
useEffect(() => {
console.log("virtual_structure is updated!");
console.log(virtual_structure);
console.log("____virtual_structure is updated!");
}, [virtual_structure]);
const refs = useRef([]);
const createStructure = () => {
console.log("virtual_structure, is it empty?");
console.log(virtual_structure);
};
useEffect(() => {
createStructure();
}, []);
const assignRef = r =>
r && (refs.current.includes(r) || refs.current.push(r));
return [assignRef, createStructure];
};
export default function App() {
const [virtual_structure, setVirtual_structure] = useState([]);
const [assignRef, updateGrid] = useMyHook(
virtual_structure,
setVirtual_structure
);
useEffect(() => {
const temp_structure = Array.from({ length: 4 }, () => ({
height: 0,
cells: []
}));
temp_structure[0].cells = Array.from({ length: 10 }, () => {
const rand = Math.random();
const r = rand > 0.1 ? parseInt(500 * rand) : parseInt(500 * 0.1);
return {
height: "",
el: (
<div ref={assignRef}>
<Image
alt=""
onload={updateGrid}
num=""
src={`https://picsum.photos/200/${r}`}
/>
</div>
)
};
});
setVirtual_structure(temp_structure);
}, []);
return (
<Container>
{virtual_structure.map((col, i) => (
<div key={`col${i}`}>
{col.cells && col.cells.map((cell, j) => <>{cell.el}</>)}
</div>
))}
</Container>
);
}
const Image = ({ alt, onload, num, src }) => (
<>
<Label>{num}</Label>
<Img src={src} alt={alt} onLoad={onload} />
</>
);
const Img = styled.img`
border: 1px solid #000;
height: min-content;
margin: 0;
padding: 0;
`;
const Label = styled.div`
position: absolute;
`;
const Container = styled.div`
width: 100%;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
background: #ccc;
align-content: center;
div {
flex: 1;
div {
color: #fff;
font-weight: 700;
font-size: 32px;
margin: 4px;
}
}
`;
And the console.log
virtual_structure is updated!
index.js:27 Array(0)length: 0__proto__: Array(0)
index.js:27 ____virtual_structure is updated!
index.js:27 virtual_structure, is it empty?
index.js:27 Array(0)
index.js:27 virtual_structure is updated!
index.js:27 Array(4)0: {height: 0, cells: Array(10)}1: {height: 0, cells: Array(0)}2: {height: 0, cells: Array(0)}3: {height: 0, cells: Array(0)}length: 4__proto__: Array(0)
index.js:27 ____virtual_structure is updated!
index.js:27 virtual_structure, is it empty?
index.js:27 Array(0)
index.js:27 virtual_structure, is it empty?
index.js:27 Array(0)
index.js:27 virtual_structure, is it empty?
index.js:27 Array(0)
index.js:27 virtual_structure, is it empty?
index.js:27 Array(0)
index.js:27 virtual_structure, is it empty?
index.js:27 Array(0)
index.js:27 virtual_structure, is it empty?
index.js:27 Array(0)
index.js:27 virtual_structure, is it empty?
index.js:27 Array(0)
index.js:27 virtual_structure, is it empty?
index.js:27 Array(0)length: 0__proto__: Array(0)
index.js:27 virtual_structure, is it empty?
index.js:27 []length: 0__proto__: Array(0)
index.js:27 virtual_structure, is it empty?
index.js:27 []
Upvotes: 2
Views: 128
Reputation: 4341
As it is said in the @Dennis-vash answer, the "closures" are freezing the useState
variable within the scope function, so that this function never sees the current (updated) value of this variable
A workaround is that instead of calling a function that executes the logic, I can always call a function that updates a state, and then use this state to trigger the function (now a useEffect instead of a function)
I will leave the question opened for few days in case that somebody would like to suggest a better alternative to solve this (?)
The code
https://codesandbox.io/s/usestate-strange-things-tneue
import React, { useRef, useState, useEffect } from "react";
import styled from "@emotion/styled";
const useMyHook = (virtual_structure, setVirtual_structure, updateGrid) => {
const refs = useRef([]);
useEffect(() => {
console.log("virtual_structure, is it empty?");
console.log(virtual_structure);
}, [updateGrid, virtual_structure]);
const assignRef = r =>
r && (refs.current.includes(r) || refs.current.push(r));
return [assignRef];
};
export default function App() {
const [virtual_structure, setVirtual_structure] = useState([]);
const [updateGrid, setUpdateGrid] = useState();
const [assignRef] = useMyHook(
virtual_structure,
setVirtual_structure,
updateGrid
);
const update = async () => setUpdateGrid(updateGrid + 1);
useEffect(() => {
const temp_structure = Array.from({ length: 4 }, () => ({
height: 0,
cells: []
}));
temp_structure[0].cells = Array.from({ length: 10 }, () => {
const rand = Math.random();
const r = rand > 0.1 ? parseInt(500 * rand) : parseInt(500 * 0.1);
return {
height: "",
el: (
<div ref={assignRef}>
<Image
alt=""
onload={update}
num=""
src={`https://picsum.photos/200/${r}`}
/>
</div>
)
};
});
setVirtual_structure(temp_structure);
}, []);
return (
<Container>
{virtual_structure.map((col, i) => (
<div key={`col${i}`}>
{col.cells &&
col.cells.map((cell, j) => (
<React.Fragment key={`cell${j}`}>{cell.el}</React.Fragment>
))}
</div>
))}
</Container>
);
}
const Image = ({ alt, onload, num, src }) => (
<>
<Label>{num}</Label>
<Img src={src} alt={alt} onLoad={onload} />
</>
);
const Img = styled.img`
border: 1px solid #000;
height: min-content;
margin: 0;
padding: 0;
`;
const Label = styled.div`
position: absolute;
`;
const Container = styled.div`
width: 100%;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
background: #ccc;
align-content: center;
div {
flex: 1;
div {
color: #fff;
font-weight: 700;
font-size: 32px;
margin: 4px;
}
}
`;
Upvotes: 0
Reputation: 53884
It happens due to closures.
You passing the updateGrid
function to every Image
component once on mount:
// useEffect closes its lexixal scope upon "updateGrid"
const [assignRef, updateGrid] = useMyHook(
virtual_structure,
setVirtual_structure
);
useEffect(() => {
...
temp_structure[0].cells = Array.from({ length: 10 }, () => {
return {
el: (
<div ref={assignRef}>
// v Used inside the callback scope
<Image onload={updateGrid} />
</div>
)
};
});
setVirtual_structure(temp_structure);
}, []);
But, the value of virtual_structure
in updateGrid
(which is createStructure
that you renamed) actually always equals to []
in the lexical scope of the useEffect
callback. Although createStructure
does updates on render, it never passed to the Image
component with the expected value.
const createStructure = () => {
console.log('virtual_structure, is it empty?');
console.log(virtual_structure); // always virtual_structure=[]
};
Side note: never ignore lint warnings, although you might know what you are doing, it may lead to unexpected bugs.
Upvotes: 2